更新记录
1.00(2026-08-25)
更新日志 1.0.0 (2026-08-25)
平台兼容性
uni-app(3.8.10)
| Vue2 | Vue3 | Chrome | Safari | app-vue | app-nvue | Android | iOS | 鸿蒙 |
|---|---|---|---|---|---|---|---|---|
| √ | √ | - | - | - | - | - | - | - |
| 微信小程序 | 支付宝小程序 | 抖音小程序 | 百度小程序 | 快手小程序 | 京东小程序 | 鸿蒙元服务 | QQ小程序 | 飞书小程序 | 小红书小程序 | 快应用-华为 | 快应用-联盟 |
|---|---|---|---|---|---|---|---|---|---|---|---|
| √ | √ | √ | √ | √ | √ | √ | √ | √ | √ | √ | √ |
Canvas 表格组件 canvas-table
基于 uni-app Canvas 2D 的高性能表格组件。用一套 Canvas 渲染引擎实现原生 <table> 做不到的事:万级数据虚拟滚动、自适应屏宽、变高行、惯性滚动、范围框选、导出长图。
零依赖:组件只依赖
canvas+uni.createSelectorQuery,不需要 sass,无需任何 npm 包。
✨ 特性
- 虚拟滚动:只绘制可视区域行,万级数据不卡(demo 用 5000 行验证)。
- 左右固定列:
fixed: 'left',横向滚动时表头与左侧列吸附。 - 单元格合并:
rowspan/colspan(全量视图下生效)。 - 自适应屏幕宽度:
fitParent测量容器真实宽度 +fit模式按屏分配列宽;支持横竖屏 / 窗口缩放自动重测重绘。 - 变高行:文本自动换行撑高,支持
minRowHeight/maxRowHeight上下限。 - 排序:点击表头排序(
sortable)。 - 搜索 / 列筛选 / 分页 / 汇总页脚:开箱即用(客户端)。
- 行多选:
selectionMode: 'multiple',已选行存对象引用,过滤/分页后不丢。 - 范围框选:拖拽橡皮筋矩形,可一键复制为 TSV。
- 惯性滚动 + 边界回弹:手机原生手感(松手继续滑、到头回弹)。
- 点击选中:单元格/行高亮,合并单元格正确映射到锚点(不会点错格)。
- 复制:单元格 / 行 / 列 / 选区,直接写入剪贴板。
- 导出图片:
canvasToTempFilePath导出可视区域,或离屏 canvas 导出整表长图。 - 自定义单元格类型:
button(按钮列)、image(图片列)、selection(多选列),以及render任意 Canvas 绘制。
📱 平台兼容
| 平台 | 兼容 |
|---|---|
| H5 | ✅ |
| App(Vue 2 / Vue 3) | ✅ |
| 微信小程序 | ✅ |
| 支付宝 / 百度 / 字节 / QQ / 快手 / 飞书小程序、快应用 | ✅(依赖各端 canvas 2d 支持) |
触摸坐标已对「页面滚动后缓存过期」「小程序 touch 用
x/y而非clientX」两类问题做兼容,手机端点击不会错位。
📦 安装 / 引入
方式一:从插件市场导入(推荐,消费者)
- 在 uni-app 插件市场 搜索
canvas-table下载。 - HBuilderX 会自动识别
uni_modules,无需手动注册,<canvas-table>即可直接使用(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'
export default {
components: { CanvasTable }
}
推荐直接用 easycom(方式一/二),模板里写
<canvas-table>即可,无需任何 import。
🚀 基本用法
<template>
<canvas-table
:columns="columns"
:data="data"
:height="480"
@cellClick="onCellClick"
@cellButton="onCellButton"
/>
</template>
<script>
export default {
data() {
return {
columns: [
{ key: 'id', title: '工号', width: 90, fixed: 'left' },
{ key: 'name', title: '姓名', width: 120, fixed: 'left' },
{ key: 'age', title: '年龄', width: 80, align: 'center', sortable: true },
{ key: 'department', title: '部门', width: 150 },
{ key: 'salary', title: '月薪', width: 130, align: 'right',
formatter: (v) => '¥' + Number(v || 0).toLocaleString() }
],
data: [
{ id: 'E1001', name: '张三', age: 28, department: '研发部', salary: 18000 },
{ id: 'E1002', name: '李四', age: 32, department: '产品部', salary: 22000 }
// ...
]
}
},
methods: {
onCellClick(e) { console.log('点击单元格', e.row, e.col.key, e.value) },
onCellButton(e) { console.log('点击按钮', e.action, e.row) }
}
}
</script>
⚙️ 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} |
点击行 |
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\ |
🧱 columns 列配置详解
每一列是一个对象,常用字段:
| 字段 | 类型 | 说明 |
|---|---|---|
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'/'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 时) |
按钮列(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 }
多选列(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 :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 }))
惯性滚动 / 边界回弹
<canvas-table :momentum="true" :momentum-friction="0.96" :rubber-band="true" ... />
松手按速度继续滑行并自然减速,滑到头带阻尼回弹。可实时关闭对比手感。
⚠️ 已知约束
- 合并单元格仅在全量视图(无过滤、无分页)生效,过滤/分页时自动失效。
- 移动端框选需先开
range-select开关(无 Shift 键);桌面端直接 Shift+拖拽。 - 目前固定列仅支持
fixed: 'left'(左侧),右侧固定列暂未提供。 - 列宽拖拽、列拖拽排序、单元格内编辑、树形/可展开行、键盘导航、右键菜单——尚未实现(规划中)。
❓ 常见问题
Q:手机端点击会"飘"到别的格子?
已做坐标兼容(页面滚动后实时刷新 canvas 基准、小程序用 touch.x/y),正常不会错位。如仍异常,请确认页面没有用 transform 整体偏移 canvas 容器。
Q:需要 sass / 其它依赖吗? 不需要。组件本身零依赖,直接用。
Q:和原生 <table> / uni-tr 比有什么优势?
所有单元格在一张 Canvas 上绘制,万级数据只渲染可视区,滚动与重绘性能远好于 DOM 表格;且天然支持导出长图、范围框选等 DOM 难做的交互。
Q:能改主题色 / 暗色吗?
可以,通过 headerBg/borderColor/headerColor/bodyColor/selectedBg 等;引擎 DEFAULTS 也集中了配色,可二开。
📤 发布到插件市场
本 uni_modules/canvas-table 目录即为可上传的插件包:
- 在 HBuilderX 中右键
uni_modules/canvas-table→「发布到插件市场」。 - 按提示填写插件市场账号信息即可。
- 上传内容以本目录的
package.json(id/displayName/版本/平台声明)为准,readme.md即插件详情页。 (若插件市场已存在同名id,请改package.json的id后重新发布。)

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