更新记录

1.1.0(2026-08-26)

1.00(2026-08-25)

更新日志 1.0.0 (2026-08-25)


平台兼容性

uni-app(3.8.3)

Vue2 Vue3 Chrome Safari app-vue app-nvue Android iOS 鸿蒙
√ √ √ √ √ √ - - -
微信小程序 支付宝小程序 抖音小程序 百度小程序 快手小程序 京东小程序 鸿蒙元服务 QQ小程序 飞书小程序 小红书小程序 快应用-华为 快应用-联盟
√ √ √ √ √ √ √ √ √ √ - √

Canvas 表格组件 canvas-table

基于 uni-app Canvas 2D 的高性能表格组件。用一套 Canvas 渲染引擎实现原生 <table> 做不到的事:万级数据虚拟滚动、自适应屏宽、变高行、惯性滚动、范围框选、导出长图,并额外提供对标 Element Table 的声明式列组件、行 / 多选 / 长按导出图片、矢量头像列等开箱即用能力。

零依赖:组件只依赖 canvas + uni.createSelectorQuery,不需要 sass,无需任何 npm 包。


✨ 特性

  • 虚拟滚动:只绘制可视区域行,2 万行数据逐级分帧加载、滚动不卡(demo 实测 20000 行流畅)。
  • 左右固定列:fixed: 'left',横向滚动时表头与左侧列吸附。
  • 单元格合并:rowspan / colspan(全量视图下生效)。
  • 自适应屏幕宽度:fitParent 测量容器真实宽度 + fit 模式按屏分配列宽;支持横竖屏 / 窗口缩放自动重测重绘。
  • 变高行:文本自动换行撑高,支持 minRowHeight / maxRowHeight 上下限。
  • 排序:点击表头排序(sortable)。
  • 搜索 / 列筛选 / 分页 / 汇总页脚:开箱即用(客户端)。
  • 行多选:selectionMode: 'multiple',已选行存对象引用,过滤/分页后不丢。
  • 范围框选:拖拽橡皮筋矩形,可一键复制为 TSV。
  • 惯性滚动 + 边界回弹:手机原生手感(松手继续滑、到头回弹)。
  • 点击选中:单元格/行高亮,合并单元格正确映射到锚点(不会点错格)。
  • 复制:单元格 / 行 / 列 / 选区,直接写入剪贴板。
  • 导出图片:可视区域导出、整表长图导出,以及点击行导出该行、多选批量导出、长按行弹窗导出三种便捷入口。
  • 声明式列(slot 写法):<canvas-table-column> 对标 element-ui <el-table-column>,父组件内用 slot 写列;复杂单元格走 :render / :formatter / :buttons / :cell-style 函数(canvas 是绘制,不能在 slot 里写 HTML,自定义内容一律走函数)。
  • 矢量头像列 type="avatar":圆形背景 + 文字纯 Canvas 绘制,H5 / 微信小程序 / App 完全一致,零异步、无图片域名 / 跨域限制。
  • 小程序图片兼容:type="image" 真实远程图在微信端自动走 createImage + getImageInfo 本地路径,真机可显示(需在小程序后台配置 downloadFile 合法域名)。
  • 自定义单元格类型:button(按钮列)、image(图片列)、avatar(矢量头像列)、selection(多选列),以及 render 任意 Canvas 绘制。

📱 平台兼容

平台 兼容
H5 ✅
App(Vue 2 / Vue 3) ✅
微信小程序 ✅
支付宝 / 百度 / 字节 / QQ / 快手 / 飞书小程序、快应用 ✅(依赖各端 canvas 2d 支持)

触摸坐标已对「页面滚动后缓存过期」「小程序 touch 用 x/y 而非 clientX」两类问题做兼容,手机端点击不会错位。 头像(type="avatar")在所有平台表现完全一致,因为它不依赖任何图片资源;而 type="image" 真实远程图在小程序真机需配置图片域名白名单。


📦 安装 / 引入

方式一:从插件市场导入(推荐,消费者)

  1. 在 uni-app 插件市场 搜索 canvas-table 下载。
  2. HBuilderX 会自动识别 uni_modules,无需手动注册,<canvas-table> 与 <canvas-table-column> 即可直接使用(easycom)。

方式二:手动放置

把本仓库的 uni_modules/canvas-table 整个目录,复制到你的项目 uni_modules/ 下即可,easycom 自动生效。

方式三:显式引入(可选)

插件放在项目根目录 uni_modules/ 下,@ 别名指向 src,所以不要写成 @/uni_modules/...。 直接从页面用相对路径引入(页面在 src/pages/xxx/ 时):

import CanvasTable from '../../../uni_modules/canvas-table/components/canvas-table/canvas-table.vue'
import CanvasTableColumn from '../../../uni_modules/canvas-table/components/canvas-table-column/canvas-table-column.vue'
export default {
  components: { CanvasTable, CanvasTableColumn }
}

推荐直接用 easycom(方式一/二),模板里写 <canvas-table> / <canvas-table-column> 即可,无需任何 import。


🚀 基本用法(两种列定义方式)

① 声明式 slot 写法(推荐,对标 Element Table)

<template>
  <canvas-table :data="data" :columns="[]" selection-mode="multiple">
    <canvas-table-column prop="id" title="工号" width="90" fixed="left" />
    <canvas-table-column prop="name" title="姓名" width="120" fixed="left" />
    <!-- 复杂单元格一律用函数绑定(canvas 是绘制,不能在 slot 写 HTML) -->
    <canvas-table-column prop="score" title="绩效" :render="scoreRender" :cell-style="scoreCellStyle" />
    <canvas-table-column prop="avatar" title="头像" width="90" type="avatar"
      avatar-text-field="avatarText" avatar-color-field="avatarColor" />
    <canvas-table-column title="操作" type="button" :buttons="opButtons" />
  </canvas-table>
</template>

② 数组 columns 写法(传统配置式)

<template>
  <canvas-table :data="data" :columns="columns" selection-mode="multiple" />
</template>
<script>
export default {
  data() {
    return {
      columns: [
        { type: 'selection', title: '', width: 48, fixed: 'left' },
        { key: 'id', title: '工号', width: 90, fixed: 'left' },
        { key: 'name', title: '姓名', width: 120, fixed: 'left' },
        { key: 'score', title: '绩效', width: 160,
          render: (ctx, p) => this.scoreRender(ctx, p),
          cellStyle: (row) => this.scoreCellStyle(row) },
        { key: 'avatar', title: '头像', width: 90, type: 'avatar',
          avatarTextField: 'avatarText', avatarColorField: 'avatarColor' },
        { title: '操作', width: 230, type: 'button', buttons: (row) => this.opButtons(row) }
      ]
    }
  },
  methods: {
    scoreRender(ctx, { value, x, y, w, h }) { /* 任意 Canvas 绘制 */ },
    scoreCellStyle(row) { return row.score >= 90 ? { color: '#67c23a' } : null },
    opButtons(row) { return [{ text: '编辑', action: 'edit', type: 'primary' }] }
  }
}
</script>

引擎 _finalColumns() 优先采用 <canvas-table-column> 子组件声明的列;若父组件未写声明式子组件,则回落到 :columns 数组。两种方式可任选其一。


⚙️ Props 属性

属性 类型 默认值 说明
canvasId String 'ctCanvas' canvas 节点 id(同页多实例需唯一)
width Number 340 宽度(px)。fitParent 为 true 时忽略
height Number 480 高度(px),决定可视区域
fitParent Boolean true true 时测量容器真实渲染宽度(推荐手机端)
fit Boolean false true 时列宽按视口宽度重新分配铺满(支持 flex/minWidth/maxWidth)
maxWidth Number 0 wrapper 最大宽度(px),fitParent 时生效
columns Array [] 列配置(见下);无声明式子组件时生效
data Array [] 源数据(数组对象)
mergedCells Array [] 合并单元格 {row,col,rowspan,colspan}
rowHeight Number 44 固定行高(autoRowHeight=false 时生效)
headerHeight Number 44 表头高度
borderColor String '#e8e8e8' 边框颜色
headerBg String/Object '#fafafa' 表头背景色;可传渐变 {from,to,direction}
headerColor String '#1f2329' 表头文字色
bodyColor String '#333333' 正文文字色
stripe Boolean true 斑马纹
fontSize Number 14 字号(px)
align String 'left' 默认对齐 left/center/right
loading Boolean false 加载态遮罩(正确叠加在数据之上,加载完成自动消失)
emptyText String '暂无数据' 空数据文案
showScrollbar Boolean true 显示滚动条
momentum Boolean true 惯性滚动
momentumFriction Number 0.96 摩擦系数(0.90~0.98,越大滑越远)
rubberBand Boolean true 边界回弹
autoRowHeight Boolean true 变高行(文本换行撑高)
minRowHeight Number 36 行高下限
maxRowHeight Number 0 行高上限(0=不限制)
lineHeight Number 20 单行文字高度(测高用)
buttonHeight Number 28 按钮列按钮高度
imageFit String 'contain' 图片填充 contain/cover
imageRadius Number 4 图片圆角
ellipsis Boolean true 非变高模式下文本省略截断
selectionMode String 'single' 'none'/'single'/'multiple' 行多选
rangeSelect Boolean false 范围框选模式(开启后拖拽=框选不滚动;桌面端也可按住 Shift 拖拽)
search String '' 全局搜索文本
pageSize Number 0 客户端分页每页条数(0=不分页)

📡 Events 事件

事件 回调参数 说明
ready — canvas 节点就绪
cellClick {rowIndex, colIndex, visualRow, visualCol, row, col, value, merge} 点击单元格
rowClick {rowIndex, row} 点击行(可用于「点击行导出该行」)
rowLongPress {rowIndex, row} 长按某行(约 450ms),可用于弹窗确认导出该行
headerClick {colIndex, col} 点击表头
cellButton {rowIndex, colIndex, action, row, col} 点击按钮列按钮
sort {key, order, data} 排序(已排好序的 data)
copy {text, mode} 复制完成
scroll {scrollX, scrollY, maxX, maxY} 滚动
selectionChange {rows, all?, row?, checked?} 行多选变化(rows 为选中行数组)
cellRangeSelect {range:{r0,c0,r1,c1}, rows} 范围框选结束

🔧 Methods 方法(通过 ref 调用)

this.$refs.table.xxx()
方法 说明
refresh() 重绘
getSelectedRows() 返回选中行数组(多选)
setSelected(row, col) 选中某单元格
setSearch(text) 设置全局搜索
setColumnFilter(key, value) 设置某列筛选
clearFilters() 清空搜索与列筛选
setPage(n) 跳到第 n 页(从 0 计)
getPageCount() 总页数
getTotal() 过滤后总条数
scrollTo(x, y) 滚动到指定位置
scrollToRow(rowIndex) 滚动到指定行(居中)
copySelection(mode) 复制:'cell'/'row'/'column'/'range',返回 Promise\
exportImage() 导出当前可视区域为图片,Promise\
exportFullImage() 导出整表长图(离屏 canvas),Promise\
exportRowImage(row, saveToAlbum?) 导出单行为图片(点击行 / 长按导出),Promise<{dataUrl?, tempFilePath?, count}>
exportRowsImage(rows, saveToAlbum?) 批量导出多行为一张图片(多选导出),Promise<{dataUrl?, tempFilePath?, count}>

导出返回值:H5 返回 dataUrl(PNG base64,组件自动触发下载);微信小程序 / App 返回 tempFilePath(临时文件,可 uni.saveImageToPhotosAlbum 保存)。


🧱 columns 列配置详解

每一列是一个对象(或对应的 <canvas-table-column> 属性),常用字段:

字段 类型 说明
key String 字段名(与 data 对应)。type:'selection'/button 可省略
title String 表头文字
width Number 列宽
minWidth Number fit 模式下最小宽度
flex Number fit 模式下瓜分剩余宽度的权重
maxWidth Number fit 模式下最大宽度
align String left/center/right
fixed 'left' 左侧固定列(需连续排在最前)
sortable Boolean 点击表头排序
formatter Function(value,row,rowIndex,colIndex) 返回展示文本
type String 'text'(默认)/'button'/'image'/'avatar'/'selection'
render Function(ctx, payload) 任意 Canvas 自定义绘制(payload 含 x,y,w,h,value,row,...)
cellStyle Function(row,rowIndex,colIndex) 返回 {bg, color} 做条件样式
summary String/Function 页脚汇总:'sum'/'avg'/'count'/'min'/'max',或 (vals,col,rows)=>文本
summaryTitle String 页脚文本前缀
filterable Boolean 是否参与全局搜索(默认 true)
filterMethod Function(value,cell,row) 列筛选判定
headerRender Function(ctx, payload) 自定义表头绘制
buttons Array/Function(row,rowIndex) 按钮列:[{text, action, type}]
imageHeight/imageWidth Number 图片列尺寸
buttonText/action/buttonType — 单按钮简化配置(无 buttons 时)
avatarTextField String 头像列文字字段名(默认 avatarText)
avatarColorField String 头像列背景色字段名(默认 avatarColor)
avatarColor String 头像列固定背景色(优先级高于 avatarColorField,高于自动配色)

按钮列(type: 'button')

{
  title: '操作', width: 230, type: 'button',
  buttons: (row) => [
    { text: '编辑', action: 'edit', type: 'primary' },
    { text: '删除', action: 'del', type: 'danger' }
  ]
}
// 点击 → @cellButton="{ action:'edit', row }"

type 取值:primary/success/danger/warning/default。

图片列(type: 'image')

{ key: 'avatar', title: '头像', width: 90, type: 'image', imageWidth: 56, imageHeight: 56, imageRadius: 8 }

微信小程序真机显示远程图需在小程序后台「开发设置 → downloadFile 合法域名」加入图片域名(开发者工具勾「不校验合法域名」可临时绕过)。

头像列(type: 'avatar',矢量绘制,推荐)

纯 Canvas 画「圆形背景 + 文字」,跨平台完全一致、无图片资源依赖:

// 最简:用 prop 字段文字 + 自动配色(无需 avatarText/avatarColor 字段)
{ key: 'name', title: '头像', width: 90, type: 'avatar' }

// 固定颜色
{ key: 'name', title: '头像', width: 90, type: 'avatar', avatarColor: '#409eff' }

// 自定义字段名(行数据里存 avatarText / avatarColor)
{ key: 'avatar', title: '头像', width: 90, type: 'avatar',
  avatarTextField: 'avatarText', avatarColorField: 'avatarColor' }
  • 文字优先取 avatarTextField(默认 avatarText),否则取 prop 字段;为空则只画圆不画字。
  • 颜色优先取 avatarColorField(默认 avatarColor),否则取 avatarColor,再否则按文字哈希自动取色(保证同文字同色)。
  • 文字超宽自动缩小字号(最低 9px),不会溢出圆圈。

多选列(type: 'selection')

{ type: 'selection', width: 60, fixed: 'left' } // 通常置为最左固定列

自定义绘制(render)

{
  key: 'progress', title: '进度', width: 160,
  render(ctx, { value, x, y, w, h }) {
    const p = Number(value || 0) / 100
    ctx.fillStyle = '#ebeef5'
    ctx.fillRect(x + 12, y + h / 2 - 4, w - 24, 8)
    ctx.fillStyle = '#67c23a'
    ctx.fillRect(x + 12, y + h / 2 - 4, (w - 24) * p, 8)
  }
}

🎯 功能示例

声明式列(canvas-table-column,对标 Element Table)

<canvas-table :data="data" selection-mode="multiple">
  <canvas-table-column type="selection" width="48" fixed="left" />
  <canvas-table-column prop="id"   title="工号" width="90" fixed="left" />
  <canvas-table-column prop="name" title="姓名" width="120" fixed="left" />
  <canvas-table-column prop="score" title="绩效" :render="scoreRender" :cell-style="scoreCellStyle" />
  <canvas-table-column prop="avatar" title="头像" width="90" type="avatar"
    avatar-text-field="avatarText" avatar-color-field="avatarColor" />
  <canvas-table-column prop="salary" title="月薪" align="right" :formatter="salaryFormatter" />
  <canvas-table-column title="操作" type="button" :buttons="opButtons" />
</canvas-table>

复杂单元格一律走函数(:render / :formatter / :buttons / :cell-style),因为 Canvas 是逐像素绘制,无法像 el-table 那样在 slot 内写 HTML。

自适应屏幕宽度(手机端推荐)

<canvas-table :columns="columns" :data="data" :fit-parent="true" :fit="true" :height="540" />

fitParent 测量容器真实宽度;fit 让列宽按屏铺满(可配合列的 flex/minWidth)。

固定列 + 合并单元格

columns: [
  { key: 'name', title: '姓名', width: 120, fixed: 'left' }, // 连续 left 固定列
  ...
]
mergedCells: [
  { row: 12, col: 2, rowspan: 2, colspan: 1 } // 第 12~13 行的「部门」列合并
]

⚠️ 合并单元格仅在「全量视图」(未过滤且未分页)时生效;开启搜索/筛选/分页时自动失效,避免坐标错位。

变高行 + 行高上下限

<canvas-table :auto-row-height="true" :min-row-height="40" :max-row-height="80" :line-height="20" ... />

排序

columns: [{ key: 'age', title: '年龄', sortable: true }, ...]
// @sort="{ key, order, data }"

搜索 / 筛选 / 分页 / 汇总

<canvas-table
  ref="table"
  :columns="columns"
  :data="data"
  :search="keyword"
  :page-size="20"
  @cellRangeSelect="onRange"
/>
// 列配置加汇总:
columns: [
  { key: 'id', title: '工号', summary: 'count', summaryTitle: '合计' },
  { key: 'age', title: '年龄', summary: 'avg' },
  { key: 'salary', title: '月薪', summary: 'sum', align: 'right' }
]
// 切换分页:this.$refs.table.setPage(1)
// 列筛选:this.$refs.table.setColumnFilter('department', '研发部')

汇总页脚吸底,按过滤后全量数据计算(工号计数、年龄均值、月薪合计)。

行多选 + 范围框选

<canvas-table
  ref="table"
  :columns="[{ type:'selection', width:60, fixed:'left' }, ...]"
  :data="data"
  selection-mode="multiple"
  :range-select="rangeOn"
  @selectionChange="onSel"
  @cellRangeSelect="onRange"
/>
// 复制选区:this.$refs.table.copySelection('range')
// 已选行:this.$refs.table.getSelectedRows()
  • 移动端框选:先开 range-select 开关(无 Shift 键);桌面端直接 Shift + 拖拽。
  • 框选模式下不滚动,与惯性互不冲突。

复制 / 导出图片(含行 / 多选 / 长按导出)

// 复制选中单元格
this.$refs.table.copySelection('cell')
// 导出当前可视区域
this.$refs.table.exportImage().then(path => uni.previewImage({ urls: [path] }))
// 导出整表长图
this.$refs.table.exportFullImage().then(path => uni.saveImageToPhotosAlbum({ filePath: path }))

// ① 点击某行导出该行(在 @rowClick 里调用)
this.$refs.table.exportRowImage(row)
// ② 多选批量导出(点击「导出选中行」按钮时调用)
this.$refs.table.exportRowsImage(this.$refs.table.getSelectedRows())
// ③ 长按行弹窗确认导出(在 @rowLongPress 里调用)
uni.showModal({
  title: '导出这一行',
  content: '是否将第 ' + (rowIndex + 1) + ' 行导出为图片?',
  success: (res) => { if (res.confirm) this.$refs.table.exportRowImage(row) }
})

惯性滚动 / 边界回弹

<canvas-table :momentum="true" :momentum-friction="0.96" :rubber-band="true" ... />

松手按速度继续滑行并自然减速,滑到头带阻尼回弹。可实时关闭对比手感。


⚠️ 已知约束

  1. 合并单元格仅在全量视图(无过滤、无分页)生效,过滤/分页时自动失效。
  2. 移动端框选需先开 range-select 开关(无 Shift 键);桌面端直接 Shift+拖拽。
  3. 目前固定列仅支持 fixed: 'left'(左侧),右侧固定列暂未提供。
  4. 列宽拖拽、列拖拽排序、单元格内编辑、树形/可展开行、键盘导航、右键菜单——尚未实现(规划中)。
  5. 声明式列 vs 数组列二选一:父组件使用了 <canvas-table-column> 子组件时,引擎以 slot 声明为准,:columns 数组会被忽略;都不写则无列可显示。
  6. 头像列(type="avatar")与图片列(type="image")区别:头像列是纯矢量绘制,无需任何图片资源、跨平台一致;图片列依赖远程/本地图片,小程序真机需配置图片域名白名单。

❓ 常见问题

Q:手机端点击会"飘"到别的格子? 已做坐标兼容(页面滚动后实时刷新 canvas 基准、小程序用 touch.x/y),正常不会错位。如仍异常,请确认页面没有用 transform 整体偏移 canvas 容器。

Q:小程序里头像/图片显示不出来?

  • 头像请用 type="avatar":纯 Canvas 矢量绘制,不依赖图片资源,H5 / 微信小程序 / App 表现完全一致,无 dataURL / 图片域名 / 跨域问题。
  • 图片列 type="image" 显示远程图:微信端引擎已自动走 createImage + getImageInfo,但仍需在小程序后台「downloadFile 合法域名」加入图片域名(开发者工具勾「不校验合法域名」可临时调试)。

Q:需要 sass / 其它依赖吗? 不需要。组件本身零依赖,直接用。

Q:和原生 <table> / uni-tr 比有什么优势? 所有单元格在一张 Canvas 上绘制,万级数据只渲染可视区,滚动与重绘性能远好于 DOM 表格;且天然支持导出长图、范围框选等 DOM 难做的交互。

Q:能改主题色 / 暗色吗? 可以,通过 headerBg/borderColor/headerColor/bodyColor/selectedBg 等;引擎 DEFAULTS 也集中了配色,可二开。

Q:两万行卡吗? 不卡。canvas 仅绘制可视区;demo 用 markRaw 去深层响应式 + rAF 分帧构造(每帧 1000 行),首屏即可跟手。


📤 发布到插件市场

本 uni_modules/canvas-table 目录即为可上传的插件包:

  1. 在 HBuilderX 中右键 uni_modules/canvas-table →「发布到插件市场」。
  2. 按提示填写插件市场账号信息即可。
  3. 上传内容以本目录的 package.json(id/displayName/版本/平台声明)为准,readme.md 即插件详情页。 (若插件市场已存在同名 id,请改 package.json 的 id 后重新发布。)

    本地「代码加密」只能做 terser 混淆(产物 -obf.zip);平台真正的加密发布由插件市场后台「加密发布」完成。


📝 更新日志

见 changelog.md

隐私、权限声明

1. 本插件需要申请的系统权限列表:

无

2. 本插件采集的数据、发送的服务器地址、以及数据用途说明:

无

3. 本插件是否包含广告,如包含需详细说明广告表达方式、展示频率:

无

暂无用户评论。