最近接了个需求:业务方要做一份线上“数据填报表”,用户打开链接后,能填几个固定单元格,其余地方全都只能看不许改。一开始他们想用问卷工具,但字段多、还要带表格计算;后来想用Excel发文件,又没法保证填写规范。绕了一圈,我把目光落在了Univer上。Univer是一个偏底层的开源在线表格引擎,渲染用Canvas,支持多种数据操作,也能定制“谁可以改哪些单元格”这类权限。这篇文章就从选型讲到落地,把“支持用户自己定义表格、只允许填写指定单元格、其余区域锁死”的方案完整拆一遍。如果你正打算做类似的在线填报、数据收集或者表格权限控制功能,这篇里的踩坑记录和代码思路应该能帮你少走不少弯路。无论是只写前端,还是准备接后端权限,这套方法都能直接参考。
1. 为什么是Univer:一次在线表格选型复盘
1.1 这个需求的门槛到底在哪
先说需求本身。业务方需要的不是一张静态报表,而是一个“可被用户自定义结构”的填报模板。也就是说,管理员要能先建一张表,定义哪些列是标题、哪些列是待填项、哪些单元格要放公式,然后把这张表分发出去,让用户在指定位置写数据。其余部分要处于只读状态,防止误改表头、删除公式、移动行列这些操作。
这个需求有两个隐形门槛。第一,权限控制必须是单元格级别,而不是整表只读。整表只读很简单,但“这个格子能填、那个格子不能填”需要一套细粒度的权限模型。第二,所有操作都要能兜底。很多表格组件只控制界面上的可点击状态,快捷键、复制粘贴、拖拽填充这些入口一旦没堵住,用户照样能改到不该改的地方。真正落到生产环境,这两点缺一不可。
1.2 为什么是Univer而不是自研或商业表格
选型时我对比过几类方案。自研一个表格编辑器完全没必要,光处理选区、滚动渲染、公式引擎就得写几个月,还不一定稳定。商业控件能力全,但价格不便宜,而且性能这么强的商业表格组件授权费很容易超预算。开源的方案里,Univer算是我试用下来综合体验比较均衡的:渲染层用Canvas自绘,不是普通的DOM表格,数据量大时滚动不会卡;它的命令系统做得比较规范,几乎所有改动都会经过命令分发,这给权限拦截留了非常好的切入点。
Univer的架构也适合做这种定制。它分成核心模块和各类插件,表格Sheet、文档Doc、幻灯片Slide是分业务的,UI和渲染相对独立。做在线填报系统时,我只用Sheet部分就够了,不会把一堆用不到的功能引进来。而且Univer的代码更新很频繁,社区活跃度肉眼可见,至少在“能自己改源码”这个层面比闭源方案踏实很多。
1.3 只读与可编辑并存的实际体验
Univer默认的表格体验和在线Excel很接近:可以点单元格、输入内容、拖动填充、右键插入行列。但我们要做的是“一部分可编辑、一部分只读”,这恰好是它能覆盖的场景。我设置锁定状态、保护范围以及自定义权限规则以后,最终用户打开表格,光标一旦落到不允许编辑的区域,输入直接被拦下来,不会出现“明明不能改,推两下又进去了”的情况。这个功能我之前在几个其他开源表格里也折腾过,要么只支持整表保护,要么锁定逻辑和样式耦合得很混乱,Univer在这块算是做得比较省心的。
2. 先搞清楚Univer的编辑权限模型
2.1 从Excel的锁定与保护说起
用过Excel的人应该知道一个概念:单元格有一个“锁定”属性,工作表有一个“保护”开关。默认情况下所有单元格都是锁定的,也就是说如果工作表不保护,锁定状态其实不生效;一旦你开启了工作表保护,所有锁定的单元格就都不能编辑了,只有你提前把某些单元格改成“不锁定”,这些区域才仍然允许编辑。
Univer保留了类似的思路。它把“单元格是否锁定”和“工作表是否处于保护状态”分两层来看。第一层是单元格样式里带一个锁定标记,第二层是Sheet级别的保护状态。要做出“只有指定区域可填”这种效果,最直接的办法就是:先把你允许填写的区域取消锁定,然后把工作表保护起来。
2.2 Univer的权限点与保护粒度
Univer在保护粒度上比传统的Excel更进一步。它不只是单纯地保护整个Sheet,还支持把权限细化到特定范围,比如只允许某个用户编辑B2:D10。这个概念对应的是权限点(Permission Point)。业务侧可以定义“谁能编辑工作簿”“谁能编辑这张表”“谁能编辑某个区域”等多层权限,然后由命令执行时统一判断。
有一点容易混淆,锁定状态和权限点解决的不是同一件事。锁定状态更像一个“默认开关”,权限点则负责“针对不同人、不同范围给例外”。在做复杂填报系统时,通常两者配合使用:用锁定+保护来挡住绝大部分不该编辑的区域,再用更细的权限点给特定用户开白名单。对大部分需求来说,前者已经能覆盖,后者是给你应对复杂业务留的后路。
2.3 我推荐的权限控制组合
根据我实际使用的经验,建议按这样的优先级来设计:先用样式锁定处理静态模板,比如表头、公式区、汇总区,把这些区域全部设为锁定;再把允许填写的区域设为非锁定;最后在Sheet上开启保护。这样无论用户选中哪里,受锁定保护的单元格都会直接拒绝编辑。如果有多用户、不同用户可填范围不一样的需求,再在权限点层面加一层判断。
需要特别提醒的是:不要只设置锁定但忘了开启保护。很多新手在这个环节翻车,样式上把“锁定”写清楚了,可Sheet保护没开,结果人家照样能改。反过来,如果你开启了保护,但又把整个工作表的锁定状态全部取消了,那等于没保护。
3. 实战:搭建一个只有指定单元格可填写的表格
3.1 初始化工程与依赖
Univer的前端工程实际上是一个npm包集合。项目初始化时先创建一个前端项目,然后安装Univer相关依赖。安装完以后,需要挂载容器并初始化一个Univer实例。下面这段代码是简化后的示意,具体包名和导出方式建议以你当前使用的Univer版本为准,因为我的经验是它的接口版本迭代确实比较快,抄老代码很可能会踩到方法不存在的问题。
import { Univer } from '@univerjs/core'; import { UniverUIPlugin } from '@univerjs/ui'; import { UniverRenderEngine } from '@univerjs/engine-render'; const univer = new Univer(); univer.registerPlugin(UniverRenderEngine); univer.registerPlugin(UniverUIPlugin, { container: document.getElementById('app') }); // 创建一个新工作簿 const workbook = univer.createWorkbook();这段代码做完以后,页面上就能出现一个可以操作的表格界面。Univer默认的UI会把工具栏、公式栏、工作表tab都渲染出来,但后续做填报场景时,工具栏那些按钮可能还需要按需求隐藏,这是后话。
3.2 创建表格模板,设置锁定与保护
接下来是关键动作:先写出表头和表格模板,再把需要保护的区域锁起来。假设我们要做一张“项目预算填报表”,表头是第一行:部门、项目、预算、已支出、使用人、日期、备注。允许填写者的范围是第二行到第五十行,列范围是B到G(即项目、预算、已支出、使用人、日期、备注)。第一列“部门”可能由系统自动生成,也不允许用户改。
const sheet = workbook.getActiveSheet(); // 写入表头 sheet.getRange('A1:H1').setValues([ ['部门', '项目', '预算', '已支出', '使用人', '日期', '备注'] ]); // 表头区域锁定 sheet.getRange('A1:H1').setLocked(true); // 部门列同样锁定 sheet.getRange('A2:A50').setLocked(true); // 允许填写区域解锁 sheet.getRange('B2:G50').setLocked(false); // 保护整个工作表 sheet.protectSheet('填报模板');这里我刻意用了比较直观的Range字符串写法。Univer实际接口里,Range参数可能要求传入行列下标对象,但思路是完全一致的。protectSheet执行以后,锁定区域就会变成只读,B2:G50仍然可以录入。
3.3 把可编辑区域展示给用户
光能编辑还不够,用户体验上最好再进一步:让用户一眼看出哪些格子可以填。我的做法是允许填写区域填充一个非常浅的背景色,并且在旁边加一列说明文字。比如把B2:G50的背景设为浅黄色,然后用批注或右侧单元格写一句“仅黄色区域可填写”。
sheet.getRange('B2:G50').setStyle({ background: '#fff8dc', border: { all: { color: '#d4c48a', style: 'thin' } } });这样做的好处很明显。用户拿到表格后不需要摸索,直接根据颜色填写。管理员在后台看的时候,也能明显区分模板区和数据区。不过要注意,背景色这种视觉引导只能作为辅助,真正的权限还是要靠锁定机制去保证,不能寄希望于“用户不会去改没变色的区域”。
4. 命令拦截:把“不能改”这件事做成硬约束
4.1 为什么UI保护不够
有朋友会问:既然保护都开了,为什么还需要命令拦截这种硬手段?因为表格操作入口实在太多了。常规点击、双击编辑只是最基础的一种,你还需要考虑快捷键操作、拖拽填充、复制粘贴、Insert行列、Delete删除内容等等。有一点很关键:保护状态可能会被某些命令绕过,或者出现多个命令组合操作的边界情况。如果只在UI层禁用按钮,基本等于留给用户各种“骚操作”的空间。
我的建议是把权限判断上升到命令层。Univer内部几乎所有操作都是通过Command触发的,你可以在一开始把要拦截的命令类型列出来,然后在命令真正执行前插入一道校验。只要命令系统统一分发入口,这道校验就能统一拦住。
4.2 在命令层加白名单
一个比较省事的伪代码如下:先定义一份可编辑区域白名单,然后监听命令服务。在写操作发生时,根据命令携带的Range信息判断是否落在白名单内。如果命中,放行;否则直接返回失败,不给用户任何修改的机会。
const allowedRanges = [ { startRow: 1, startCol: 1, endRow: 49, endCol: 6 } ]; function isAllowed(range) { return allowedRanges.some((r) => { return range.startRow >= r.startRow && range.endRow <= r.endRow && range.startCol >= r.startCol && range.endCol <= r.endCol; }); } commandService.beforeCommandExecute((command) => { if (command.type === 'sheet.setRangeValues' || command.type === 'sheet.setCellContent') { const ranges = command.payload.ranges ?? [command.payload.range]; for (const range of ranges) { if (!isAllowed(range)) { return false; } } } return true; });这段代码的重点在于“遍历命令携带的所有Range”。很多实际事故都出在只检查了第一个Range,用户框选一片跨了锁定和非锁定的区域后,部分被拦截部分却被放行,数据写了一半,很难看。
4.3 复制粘贴、拖拽填充这些隐藏入口
复制粘贴是个很容易被忽略的入口。如果在粘贴时触发的是PasteCommand或者SetRangeValues,拦截逻辑同样能覆盖到。但要注意顺序:粘贴前往往还伴随着复制、剪切等操作,最好对粘贴的目标Range做独立校验。拖拽填充也有类似的问题,拖拽填充通常会把一个公式或值复制到多个单元格,这时候要检查目标区域是否整体在白名单内。
我踩过这样一个坑:用户填好两行数据后,想用右下角下拉手柄复制上一行的公式,结果目标区域包含了几个锁定的汇总列。由于我只校验了命令的起始Range,后续被填充的区域没有逐个判断,导致汇总列被改动。后来改成“遍历所有目标Range”才彻底解决。这个教训分享出来,就是希望你别在同一个坑里栽一次。
4.4 自定义提示与用户体验
命令被拦截后,如果用户一点反应都没有,体验会很差。建议在拦截时抛出一个带提示的返回值,让前端UI弹出提示“该区域不可编辑”。Univer的CommandService返回的结构里可以带上message,UI层拿到以后展示成toast或者modal。这一步做得好,用户能明确知道是权限问题,而不是觉得表格坏了。
5. 常见问题与排查技巧实录
5.1 锁定了但还能编辑怎么办
这是一个出现频率极高的问题。排查顺序如下:先确认工作表保护是否真的开启,再确认目标单元格的锁定状态是否是“非锁定”。很多情况下是因为你锁定了表头,却忘了把保护状态开起来;或者反过来,你以为某个单元格已经解锁,实际它继承的是整行/整列的锁定状态。最简单的方法是选中一个不允许编辑的单元格,看看它的锁定状态属性。如果是锁定且保护已开,那它一定不能编辑。
5.2 非编辑区域还能选中、复制怎么办
这是另一个容易误会的地方。很多需求方提“不能修改”时,往往也要求“不能复制、不能选取”。但编辑权限和选择权限其实是互相独立的能力。Univer里禁止编辑不等于禁止选中,用户仍然可以点选、复制数据,只是没法改。如果连复制都不允许,需要在Selection层面再加一层限制,或者在后端做水印等隔离方案。我建议和业务方提前对齐这一点,别到了上线才发现理解不一致。
5.3 多人同时编辑,权限如何不打架
如果是纯前端本地表格,这个问题还好办。一旦接到协同场景,用户A和用户B同时编辑同一张表,权限判断必须统一放在服务端命令入口,不能依赖各自浏览器的状态。前端填好的数据也要通过协同通道同步。Univer对协同编辑有支持,OT或CRDT类机制都有涉及,但权限规则最后还是要后端校验一遍,防止有人绕过前端直接调用接口。
5.4 大数据量下的性能注意事项
虽然Univer基于Canvas渲染,滚动流畅度不错,但如果你的业务表动不动就是几十万行,权限校验就得多留个心眼。每次编辑前都遍历白名单大范围,在数据量大时会有额外开销。建议把可编辑范围做成索引或者缓存,比如按行号分桶,判断某个Range是否可编辑时,先用二分找可能命中的桶,再判断具体行列,能省掉不少循环。
6. 拿这套东西还能做什么
6.1 从报表收集到数据回填
在线填报只是低门槛的一步。做完了这一套,你会发现很多数据流通的场景都能直接复用。管理员定义一张表格模板,分发给各个业务人员填写,收集上来以后又可以在后台做汇总计算。要是模板里加上了下拉列表、数据校验,填出来的数据质量能有明显提升。
6.2 模板分发与回收
Univer天然适合做“模板分发与回收”的逻辑。你可以准备多套表格模板,在模板里锁定好字段结构,只留数据区给填写者。到时间以后回收表格,校验填入的内容,再往业务系统里回流。因为模板结构是锁死的,直接省去了清洗脏数据的不少麻烦。
6.3 扩展:把权限规则交给后端
如果你只做单个表格,前端写死白名单问题不大。但真正的多用户系统里,不同角色可编辑区域往往不一样。我的建议是把“可编辑范围”配置化,由后端接口返回当前用户的可编辑范围,前端拿到这份配置后统一灌入权限服务和命令拦截器。这样以后要让某个人多填一列、少填几行,改数据库配置就行,不用发版。
最后再分享一个小技巧:做这种在线表格时,不要只盯着“能不能编辑”这一个状态,还要考虑“能不能选中”“能不能复制”“能不能看公式”这些维度。我在实际项目中就是先列出一张“表格操作权限清单”,逐个确认需求,再在Univer里配置对应拦截。这套方法比开发到一半再补权限要省心得多,尤其当你负责的是一个长期演进的数据收集系统,前期多花半小时,后期能省下好几个加班夜。