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

@qfei-design/canvas-table

v1.3.0

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 项目自动安装和接入,请优先阅读:

推荐读取顺序:

  1. package.ai.json
  2. docs/agent-usage.md
  3. recipes.json
  4. capabilities.json
  5. PUBLIC_API.md
  6. 按场景选择最小示例

最快成功路径:

最快成功路径:先读 docs/agent-usage.md 选择接入路径,再从对应 recipe 指向的最小示例起步。

能力目录

核心能力文档:

进阶文档:

核心导出

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, request) 回填数据。

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, request) => {
    request.useRequestIdentity();
    try {
      const rows = await fetchRows(page, { signal: request.signal });
      table.setData(rows, page, request);
    } catch (error) {
      table.markPageLoadFailed(page, request);
      if (request.signal.aborted) return;
      console.error("Failed to load virtual table page", {
        page,
        requestId: request.requestId,
        errorName: error instanceof Error ? error.name : typeof error,
      });
    }
  },
);

推荐分页接入顺序:

  1. 先调用 fetchCount() 获取总数
  2. 用返回值设置 virtualOptions.totalRowCount
  3. 再监听 data:load
  4. 同步调用 request.useRequestIdentity(),并在成功/失败时把同一个 request 传给 setData / markPageLoadFailed

如果是 React / Next.js 客户端组件,优先直接参考:

这个示例明确包含:

  • fetchTotalCount()totalRowCount 的初始化关系
  • 请求上下文与 setData(rows, page, request) 的回填关系
  • 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 容器来创建 CanvasTableComponentGroupTableComponent

常见接入方式

普通表格接入

适合非分组场景。通常流程是:

  1. 创建 CanvasTableComponent
  2. 传入列定义和画布尺寸
  3. 不传 virtualOptions,首次直接调用 setData
  4. 按需监听滚动、选择、编辑和加载事件

分页加载接入

适合大数据量场景。通常流程是:

  1. 开启 virtualOptions
  2. 监听 data:load
  3. 按页请求数据
  4. 认领请求身份并使用 setData(data, page, request) 回填

如果是 React 接入,加载回调应直接返回或回填对应请求的数据,避免用一份共享 data/dataPage 状态承接多个并发页。

注意:分页模式下,setData 必须传 page,否则会抛错。

分组表格接入

适合分层展示场景。通常流程是:

  1. 创建 GroupTableComponent
  2. 配置 groupsgroupVirtualOptions
  3. 监听 group:load
  4. 调用 setGroup(data, group, page) 回填分组
  5. 叶子明细分页时监听 group:data:load
  6. 通过 setData(data, group, page, { totalRowCount, request }) 回填叶子页
  7. 请求失败或取消时调用 markGroupPageLoadFailed(group, page, request)

自定义渲染

列上可以通过 renderheaderRender 进行扩展。

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: 当前支持 left
  • editType: input | custom
  • render: 自定义单元格渲染
  • 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: 序号列配置;showSN.width 可设置内置行头/序号列最终宽度,未设置时沿用默认自动宽度
  • rowSortable: 行拖拽排序配置
  • selectable: 行选择配置
  • rowStyleOptions: 行样式计算
  • emptyStateOptions: 空状态配置
  • showEmptyCellPlaceholder: 空值单元格是否展示默认占位符 -,默认展示;传 false 时空值不展示横线
  • customEdit: 自定义编辑器
  • editApplyMode: 编辑写入模式,支持 auto | controlled
  • showHeader / showBody: 是否渲染表头或表体

序号列说明:

  • showSN.width 只在 showSN.enabled = true 时生效。
  • 设置后该值会作为内置固定行头列的最终宽度,不会再根据复选框、行拖拽图标或行头后缀自动扩容。
  • 如果同时启用 selectablerowSortablebodyRowHeadSuffixOptions,宿主需要自行预留足够宽度。

空值单元格说明:

  • 默认情况下,单元格值为 nullundefined 或空字符串 '' 时展示 -
  • 设置 showEmptyCellPlaceholder: false 后,空值单元格不展示横线。
  • 0false 会作为有效值展示为 0false,不会被当成空值。

CanvasTableComponent

常用方法:

  • updateProps(newProps)
  • updateCanvasSize(width, height, refresh?)
  • refreshCanvas()
  • setData(data, page?, request?)
  • setVirtualPageData(data, page, totalRowCount, request?)
  • markPageLoadFailed(page, request?)
  • 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),支持普通对象和保留数字/字符串键类型的 ReadonlyMap
  • getTableData()
  • refreshEmptyState()
  • updateSummaryData(newSummaryData)
  • setSummaryLoading(loading, columnKey?)
  • getSelectionInfo()
  • clearSelection()
  • destroy()

updateDataByRowKey(dataMap) 使用普通对象时遵循 JavaScript 对象键语义:优先匹配字符串键,仅在没有同名字符串行键时把规范数字字符串回退为数字键。需要严格区分 "1"1 时请传入 ReadonlyMap。每条替换数据必须保留当前配置解析出的原 rowKey;方法会先校验全部命中行,任一行尝试变更身份时整批拒绝,不会产生部分更新。

setRowData()setCellData() 和默认自动编辑同样不能修改当前配置解析出的 rowKey。写入会先基于候选行校验身份,身份变化时抛错且保留原行数据;需要更换行身份时,请重新提交数据集,而不是增量修改身份字段。

GroupTableComponent

在基础表格能力之外,额外提供:

  • setGroup(data, group?, page?)
  • setData(data, groups, page?, options?),分页请求上下文通过 options.request 回传
  • setDataTotal(count),显式设置分组表共享选择总数
  • markGroupPageLoadFailed(groupValue, page, request?)
  • 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()

调用 updateProps({ virtualOptions }) 会同步更新已经创建的所有叶子表。分页开关、页大小和缓存上限使用新配置,各叶子的 totalRowCount 仍以对应分组的明细总数为准。分页模式或非分页模式显式提供正数 totalRowCount 时,共享选择总数使用该外部总数;非分页且未提供正数总数时,由已加载一级分组的 count 之和统一派生。setDataTotal(count) 可显式接管总数,后续叶子回包和分组加载不会覆盖它。分组表头不会进入叶子明细虚拟分页模式。

事件与扩展

事件总线

通过 globalEventBus 可以监听实例级事件。

常见事件包括:

  • data:load: 请求某一页数据,payload 为 page 和可选认领的请求上下文
  • group:load: 请求某个分组的子分组数据
  • group:data:load: 请求某个叶子分组下的一页明细数据,payload 为分组路径、page 和请求上下文
  • group:expand: 分组展开/收起
  • scroll:change: 滚动变化
  • selection:change: 选择变化
  • selection:range-limit: 单次 Shift 区间超过 200 行,返回 source、限制值和区间索引,宿主可据此展示 toast
  • selection:range-error: Shift 区间缺页加载或 ID 校验失败,返回 source、操作 ID、区间索引、失败原因和可选失败页码
  • edit:start: 编辑开始
  • edit:change: 编辑值变更
  • edit:cancel: 编辑取消
  • edit:end: 编辑结束
  • copy: 复制完成,返回 successrowCountcolumnCountcellCount,不返回复制文本
  • paste: 粘贴完成
  • row:* / cell:* / column:*: 行、单元格、列的鼠标交互事件
  • shape:*: Shape 鼠标交互事件

示例:

const off = globalEventBus.onWithNamespace(
  "selection:change",
  table.tableId,
  (payload) => {
    console.log(payload);
    if (payload.change?.source === "shift-range") {
      console.log("本次 Shift 区间 ID", payload.change.rangeIds);
    }
  },
);

// 取消监听
off();

Shape 与动画

库内导出了 Shape 系统,可用于更复杂的单元格图形绘制和动画。

进阶说明请参考:

已知约束

  • 当前包是 client-only,不支持 SSR 直接实例化。
  • 虽然核心库不依赖 Vue 运行时,但现有仓库 demo 主要是 Vue 页面。
  • 分页虚拟滚动模式下,setData 必须传 page
  • 分组模式下,外部需要分别维护分组加载和组内数据加载。
  • 编辑、粘贴、选择等业务提交逻辑需要接入方基于事件自行承接。
  • 当前仓库中 demo 较完整,但自动化测试仍偏少,更适合作为业务底座持续演进。

Demo 与源码参考