更新记录
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"真实远程图在小程序真机需配置图片域名白名单。
📦 安装 / 引入
方式一:从插件市场导入(推荐,消费者)
- 在 uni-app 插件市场 搜索
canvas-table下载。 - 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" ... />
松手按速度继续滑行并自然减速,滑到头带阻尼回弹。可实时关闭对比手感。
⚠️ 已知约束
- 合并单元格仅在全量视图(无过滤、无分页)生效,过滤/分页时自动失效。
- 移动端框选需先开
range-select开关(无 Shift 键);桌面端直接 Shift+拖拽。 - 目前固定列仅支持
fixed: 'left'(左侧),右侧固定列暂未提供。 - 列宽拖拽、列拖拽排序、单元格内编辑、树形/可展开行、键盘导航、右键菜单——尚未实现(规划中)。
- 声明式列 vs 数组列二选一:父组件使用了
<canvas-table-column>子组件时,引擎以 slot 声明为准,:columns数组会被忽略;都不写则无列可显示。 - 头像列(
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 目录即为可上传的插件包:
- 在 HBuilderX 中右键
uni_modules/canvas-table→「发布到插件市场」。 - 按提示填写插件市场账号信息即可。
- 上传内容以本目录的
package.json(id/displayName/版本/平台声明)为准,readme.md即插件详情页。 (若插件市场已存在同名id,请改package.json的id后重新发布。)本地「代码加密」只能做 terser 混淆(产物
-obf.zip);平台真正的加密发布由插件市场后台「加密发布」完成。

收藏人数:
购买普通授权版(
试用
赞赏(0)
下载 204
赞赏 2
下载 12663358
赞赏 1955
赞赏
京公网安备:11010802035340号