更新记录

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」两类问题做兼容,手机端点击不会错位。


📦 安装 / 引入

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

  1. uni-app 插件市场 搜索 canvas-table 下载。
  2. 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" ... />

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


⚠️ 已知约束

  1. 合并单元格仅在全量视图(无过滤、无分页)生效,过滤/分页时自动失效。
  2. 移动端框选需先开 range-select 开关(无 Shift 键);桌面端直接 Shift+拖拽。
  3. 目前固定列仅支持 fixed: 'left'(左侧),右侧固定列暂未提供。
  4. 列宽拖拽、列拖拽排序、单元格内编辑、树形/可展开行、键盘导航、右键菜单——尚未实现(规划中)。

❓ 常见问题

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 目录即为可上传的插件包:

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

📝 更新日志

changelog.md

隐私、权限声明

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

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

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

暂无用户评论。