@qfei-design/canvas-table
v1.1.5
Published
A high-performance canvas-based table library for browser web apps
Readme
@qfei-design/canvas-table
基于 Canvas 的高性能表格库,适合大数据量、复杂单元格绘制和高交互场景。
适用场景
- 数据量大,DOM 表格渲染成本高
- 单元格样式复杂,需要图形化渲染能力
- 需要固定列、汇总、选择、编辑、拖拽等交互
- 需要分组表格或二次扩展渲染能力
核心能力
- 基于 Canvas 渲染,适合高密度表格场景
- 支持虚拟滚动和分页加载
- 支持左侧固定列
- 支持表头列拖拽重排
- 支持行拖拽排序
- 支持汇总行与汇总 loading 状态
- 支持单元格编辑和自定义编辑器
- 支持自定义单元格/表头渲染
- 支持分组表格
- 提供事件总线,便于接入方监听滚动、加载、编辑、选择等行为
- 提供 Shape 与动画能力,用于复杂视觉扩展
安装
pnpm add @qfei-design/canvas-table这是一个浏览器端运行的 client-only 包,核心实例可以用于 React、Vue 或原生 DOM 场景。
默认是本地数据模式:不传 virtualOptions 时,可直接调用 setData(rows) 渲染数据。
只有显式传入 virtualOptions.enabled = true 时,才进入虚拟分页加载模式。
AI / 自动集成说明
如果这个包需要被另一个 AI 项目自动安装和接入,请优先阅读:
- docs/agent-usage.md
- recipes.json
- capabilities.json
- AI_INTEGRATION.md
- PUBLIC_API.md
- package.ai.json
- docs/features/meta-adapter.md
- React 基础示例
- React 虚拟分页示例
- React 分组表示例
- React Meta Adapter 示例
- Vue 示例
推荐读取顺序:
package.ai.jsondocs/agent-usage.mdrecipes.jsoncapabilities.jsonPUBLIC_API.md- 按场景选择最小示例
最快成功路径:
最快成功路径:先读 docs/agent-usage.md 选择接入路径,再从对应 recipe 指向的最小示例起步。
能力目录
核心能力文档:
- basic-table: 基础表格接入和本地数据渲染
- virtual-scroll: 大数据量分页虚拟滚动
- group-table: 分组表格与组内明细加载
- row-selection: 单选、多选和选择态读取
- row-drag: 行拖拽排序和当前顺序读取
- cell-edit: 内置输入编辑和自定义编辑器
- summary-row: 汇总行、汇总 loading 和外部更新
- fixed-columns: 左侧固定列和宽表横向滚动
- column-drag: 表头拖拽换列和宽表列编排
- empty-state: 空状态文案、图片和自定义绘制
- event-bus:
globalEventBus事件订阅和实例级监听
进阶文档:
- ANIMATION_USAGE.md: Shape 与动画扩展
- docs/features/meta-adapter.md: 当上游字段定义来自 JSON meta 时,如何先转换为
IColumn[]再接入表格 - React Meta Adapter 示例: 展示如何把 JSON meta 转成
IColumn[]并在运行时切换列定义
核心导出
import {
CanvasTableComponent,
GroupTableComponent,
TextShape,
globalEventBus,
} from '@qfei-design/canvas-table'
import type {
IColumn,
TableCanvasProps,
GroupTableProps,
} from '@qfei-design/canvas-table'还可以直接使用库内导出的 Shape、工具函数和图标资源。
快速开始
1. 普通表格
import { CanvasTableComponent } from '@qfei-design/canvas-table'
import type { IColumn } from '@qfei-design/canvas-table'
const container = document.getElementById('table-root')!
const columns: IColumn[] = [
{ key: 'id', title: 'ID', width: 80, headerAlign: 'center', align: 'center', fixed: 'left' },
{ key: 'name', title: '姓名', width: 160, editType: 'input', headerAlign: 'left', align: 'left' },
{ key: 'email', title: '邮箱', width: 240, headerAlign: 'right', align: 'right' },
]
const table = new CanvasTableComponent(container, {
columns,
canvasWidth: 960,
canvasHeight: 600,
rowKey: 'id',
style: {
rowHeight: 40,
headerHeight: 40,
fontSize: 14,
fontFamily: 'Arial, sans-serif',
},
})
table.setData([
{ id: 1, name: '张三', email: '[email protected]' },
{ id: 2, name: '李四', email: '[email protected]' },
])说明:这条路径默认是“本地数据直出”,不需要 data:load 事件,也不需要传 page。
2. 虚拟滚动分页加载
启用分页加载后,推荐先请求一次 count,再用总数初始化 virtualOptions.totalRowCount。随后外部监听 data:load 事件,并在回调中调用 setData(data, page) 回填数据。
const table = new CanvasTableComponent(container, {
columns,
canvasWidth: 960,
canvasHeight: 600,
rowKey: 'id',
style: {
rowHeight: 40,
headerHeight: 40,
fontSize: 14,
fontFamily: 'Arial, sans-serif',
},
virtualOptions: {
enabled: true,
totalRowCount: 10000,
pageSize: 100,
},
})
const off = globalEventBus.onWithNamespace('data:load', table.tableId, async (page) => {
const rows = await fetchRows(page)
table.setData(rows, page)
})推荐分页接入顺序:
- 先调用
fetchCount()获取总数 - 用返回值设置
virtualOptions.totalRowCount - 再监听
data:load - 在回调中按页请求并
setData(rows, page)
如果是 React / Next.js 客户端组件,优先直接参考:
这个示例明确包含:
fetchTotalCount()与totalRowCount的初始化关系dataPage与setData(rows, page)的回填关系data:load的订阅方式page + 1的后端分页转换位置
3. 分组表格
import { GroupTableComponent } from '@qfei-design/canvas-table'
const groupTable = new GroupTableComponent(container, {
columns,
groups: ['department', 'team'],
canvasWidth: 960,
canvasHeight: 600,
rowKey: 'id',
style: {
rowHeight: 40,
headerHeight: 40,
fontSize: 14,
fontFamily: 'Arial, sans-serif',
},
groupVirtualOptions: {
enabled: true,
totalRowCount: 50,
pageSize: 20,
},
virtualOptions: {
enabled: true,
totalRowCount: 0,
pageSize: 100,
},
})
globalEventBus.onWithNamespace('group:load', groupTable.tableId, async (group, page) => {
const groups = await fetchGroups(group, page)
groupTable.setGroup(groups, group, page)
})框架适配
React
推荐做法是封装一个 React 组件,在 useEffect 中创建和销毁 CanvasTableComponent。
可直接参考:
Vue
推荐在挂载后创建实例,在卸载前销毁实例,并通过外部状态管理数据加载和事件订阅。
可直接参考:
Vanilla DOM
如果不是 React 或 Vue,也可以直接传入真实 DOM 容器来创建 CanvasTableComponent 或 GroupTableComponent。
常见接入方式
普通表格接入
适合非分组场景。通常流程是:
- 创建
CanvasTableComponent - 传入列定义和画布尺寸
- 不传
virtualOptions,首次直接调用setData - 按需监听滚动、选择、编辑和加载事件
分页加载接入
适合大数据量场景。通常流程是:
- 开启
virtualOptions - 监听
data:load - 按页请求数据
- 使用
setData(data, page)回填
如果是 React 接入,建议额外保留一个 dataPage 状态,确保回填时传回表格请求的同一页码。
注意:分页模式下,setData 必须传 page,否则会抛错。
分组表格接入
适合分层展示场景。通常流程是:
- 创建
GroupTableComponent - 配置
groups和groupVirtualOptions - 监听
group:load - 调用
setGroup(data, group, page)回填分组 - 组内明细数据通过
setData(data, group, page)回填
自定义渲染
列上可以通过 render 和 headerRender 进行扩展。
import { TextShape } from '@qfei-design/canvas-table'
const columns: IColumn[] = [
{
key: 'status',
title: '状态',
width: 120,
render(cell, group) {
group.addChild(
new TextShape({
name: 'status-text',
x: 12,
y: 24,
attrs: {
text: String(cell.rowData.status ?? ''),
fill: '#1f2937',
fontSize: 14,
},
}),
)
},
},
]自定义编辑器
当列设置 editType: 'custom' 时,可以通过 customEdit 自定义编辑行为。
const table = new CanvasTableComponent(container, {
columns: [
{ key: 'name', title: '姓名', width: 160, editType: 'custom' },
],
canvasWidth: 960,
canvasHeight: 600,
style: {
rowHeight: 40,
headerHeight: 40,
fontSize: 14,
fontFamily: 'Arial, sans-serif',
},
customEdit({ value, updateValue, commit, cancel }) {
const input = document.createElement('input')
input.value = String(value ?? '')
input.addEventListener('input', () => {
updateValue(input.value)
})
return {
element: input,
getValue: () => input.value,
autoClose: true,
destroy: () => {
input.remove()
},
}
},
})复杂编辑器可以让 canvasTable 继续管理关闭和键盘行为,只把弹窗根节点声明给表格:
customEdit({ value, commit, cancel }) {
const editor = document.createElement('div')
const popupRoot = document.createElement('div')
document.body.appendChild(popupRoot)
return {
element: editor,
getValue: () => value,
autoClose: {
outsideClick: 'commit',
escape: 'cancel',
enter: 'commitAndMove',
tab: 'commitAndMove',
},
relatedElements: () => [popupRoot],
overlayOptions: {
overflow: 'visible',
},
destroy: () => {
popupRoot.remove()
},
}
}编辑写入默认由表格自动完成。若业务需要先走保存/草稿层,再由宿主在成功后回填数据,可设置 editApplyMode: 'controlled',此时表格只发出编辑事件,不主动改写行数据。
核心 API
IColumn
常用字段:
key: 列唯一标识title: 表头标题width: 列宽minWidth: 最小列宽align: 单元格对齐方式headerAlign: 表头对齐方式fixed: 当前支持lefteditType:input | customrender: 自定义单元格渲染headerRender: 自定义表头渲染prefixRender/suffixRender: 表头前后缀图标suffixRender返回值可设置displayMode: 'hover',让后缀图标仅在鼠标悬浮表头单元格时展示;默认常显suffixRender.onClick接收(column, cell, event),可用cell和原生点击事件定位宿主弹窗;点击后缀图标不会继续触发表头列点击stateChange/headerStateChange: 状态变化回调
对齐规则:
headerAlign只影响表头内置文本布局align影响表体与汇总行内置文本布局- 两者都支持
left | center | right - 未传时默认左对齐
TableCanvasProps
常用字段:
columns: 列配置canvasWidth/canvasHeight: 画布尺寸style: 表格样式rowKey: 行唯一标识virtualOptions: 虚拟滚动配置showSummary: 是否展示汇总行summaryRowHeight: 汇总行高度summaryData: 汇总数据summaryRenderer: 汇总渲染函数showSN: 序号列配置rowSortable: 行拖拽排序配置selectable: 行选择配置rowStyleOptions: 行样式计算emptyStateOptions: 空状态配置customEdit: 自定义编辑器editApplyMode: 编辑写入模式,支持auto | controlledshowHeader/showBody: 是否渲染表头或表体
CanvasTableComponent
常用方法:
updateProps(newProps)updateCanvasSize(width, height, refresh?)refreshCanvas()setData(data, page?)scrollTo(x, y)scrollToRow(rowIndex)getScrollState()setColumnOption(colKey, key, value)setRowColors(rowKeys, color?)setRowTip(rowKey, params?)setRowData(rowKey, data)setCellData(rowKey, columnKey, data)clearData(params?)getPagesInView()updateDataByRowKey(dataMap)getTableData()refreshEmptyState()updateSummaryData(newSummaryData)setSummaryLoading(loading, columnKey?)getSelectionInfo()clearSelection()destroy()
GroupTableComponent
在基础表格能力之外,额外提供:
setGroup(data, group?, page?)setData(data, groups, page?)scrollTo(x, y, animate?)scroll(x, y, animate?)setRowTip(groupValue, rowKey, params?)setRowData(groupValue, rowKey, data)setCellData(groupValue, rowKey, columnKey, data)extendGroup(groupValue, expand)clearData()clearSelection()getSelectionInfo()destroy()
事件与扩展
事件总线
通过 globalEventBus 可以监听实例级事件。
常见事件包括:
data:load: 请求某一页数据group:load: 请求某个分组的子分组数据group:expand: 分组展开/收起scroll:change: 滚动变化selection:change: 选择变化edit:start: 编辑开始edit:change: 编辑值变更edit:cancel: 编辑取消edit:end: 编辑结束paste: 粘贴完成row:*/cell:*/column:*: 行、单元格、列的鼠标交互事件shape:*: Shape 鼠标交互事件
示例:
const off = globalEventBus.onWithNamespace('selection:change', table.tableId, (payload) => {
console.log(payload)
})
// 取消监听
off()Shape 与动画
库内导出了 Shape 系统,可用于更复杂的单元格图形绘制和动画。
进阶说明请参考:
已知约束
- 当前包是 client-only,不支持 SSR 直接实例化。
- 虽然核心库不依赖 Vue 运行时,但现有仓库 demo 主要是 Vue 页面。
- 分页虚拟滚动模式下,
setData必须传page。 - 分组模式下,外部需要分别维护分组加载和组内数据加载。
- 编辑、粘贴、选择等业务提交逻辑需要接入方基于事件自行承接。
- 当前仓库中 demo 较完整,但自动化测试仍偏少,更适合作为业务底座持续演进。
Demo 与源码参考
- 仓库说明:../../readme.md
- demo 应用:
packages/app - 动画文档:ANIMATION_USAGE.md
- feature 文档:
docs/features/*.md
