ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

用Univer实现在线表格填表:锁定单元格与模板权限控制的完整实战

用Univer实现在线表格填表:锁定单元格与模板权限控制的完整实战 去年我接了一个内部系统的需求要给业务部门做一套在线填表工具。业务方的想法很朴素把原来的Excel模板搬到网页上用户打开链接就能填但只能填指定的几个格子其他单元格公式区、表头、说明区绝对不能碰。第一反应是做一套表单前端可字段一多、模板一改反而变成无底洞。后来我翻开源项目找到了Univer。Univer是一款开源的在线电子表格引擎底层用TypeScript加Canvas渲染交互体验很接近Excel并且支持插件化扩展。它最打动我的一点是支持用户自定义表格模板同时可以用工作表保护功能把绝大部分单元格锁死只留指定范围给用户填写——这不就是业务方要的东西吗这篇内容我会从Univer的基础认知讲起详细拆解“定义模板锁定单元格在线填写”的实现路径把我在实战中踩过的坑也一并整理出来适合正在做数据填报、订单登记、流程审批等表格类产品的前端开发者参考。1. 为什么在线表格类需求最终选了Univer1.1 从Excel传文件的噩梦说起先说需求背景。业务部门的日常操作是运营维护一份Excel模板然后通过邮件或者群聊反复发下去。回收上来的表格不是有人把公式给抹了就是合并单元格错位还有人在备注栏里写了一大段话把列宽全撑爆。每周汇总的时候运营小姑娘要花半天时间手工整理这些乱七八糟的表格。这个痛不是个别人的痛几乎每个跟Excel协作打交道的人都遇到过。公司后来要求把这类流程搬到线上数据填报系统逐渐成了标配。Excel这套玩法的问题其实很典型。第一模板没有版本管理你永远不知道别人填的是哪个版本第二格式约束是“软”的用户想改就能改公式也只是本地草稿第三数据都在各自的文件里汇总全靠人工复制粘贴。这三个问题不是靠Excel技巧能根治的必须换一种载体——让用户直接在同一个在线文档上操作模板锁死数据自动入库。1.2 Univer到底是什么Univer是由 dream-num 团队开源的一套在线表格解决方案核心用TypeScript编写渲染层走Canvas因此在网页上的操作手感能跟桌面软件比较接近。在2.x版本里Univer把文档、表格、幻灯片三个模块都纳入了统一架构你可以只引入表格模块就能跑起来。简单说Univer就是一套能在浏览器里流畅运行、支持类Excel操作、给出丰富API和插件机制的开源基础设施。它支持的功能包括公式、样式、合并单元格、数据验证、多选区、查找替换等常用能力也支持基于Yjs的协同编辑。Univer的组件化设计做得不错渲染引擎、公式引擎、网络层、业务插件彼此可拆分所以你可以按需打包甚至把公式引擎放到Node环境里单独使用。这些特性让很多团队拿它做私有化部署的在线表格或者当作自研表格产品的地基。1.3 对比几个替代方案Univer赢在哪儿做调研的时候我专门把主流方案放一起比过一轮。方案在线填写单元格级锁定模板自定义私有化部署Excel模板传阅不支持支持但需本地操作强不涉及在线商业表格服务类似Google Sheets支持支持强不支持完全自研前端表格支持需要自己开发弱支持Univer支持支持强支持对比下来Univer赢在“组合拳”它既提供了Excel级的表格交互又允许你通过命令机制对单元格做细粒度权限控制还能私有化部署。尤其是“单元格锁定”这个能力很多开源表格项目其实没有做透有的只能锁定整个sheet无法做到某个区域可编辑、其他区域只读。Univer的思路跟Excel一致单元格本身有locked属性工作表有保护开关两者配合就能做到任意区域的读写控制。这正好切中了填表类产品的命门。2. 核心需求拆解你要的其实不是在线表格是“模板权限回收”2.1 需求听起来简单做起来全是细节表面上看这个需求只涉及一个功能点把表格放到网页上。但你把用户的使用路径拉一遍会发现它其实是三个层面的工作。模板层谁来定义表格长什么样表头、公式、样式怎么维护怎么让非技术的业务人员也能调整权限层哪些单元格是用户的可编辑区域哪些是只读区域用户点了只读区域时界面给什么反馈数据层用户填完后数据怎么收集、怎么校验、怎么回写。三个层面缺一个产品都会变成半成品。尤其是模板层如果只做成“开发前把表格配死”需求又会退回到Excel传阅的老路上去业务方想要自己调整模板时依然要求人。所以真正要做的是一套“模板可配置、权限可控制、数据可回收”的在线填表方案。2.2 典型场景不只是填表还有订单、审批、打分这类需求其实到处都是。比如一个流程审批系统里财务需要填报报销单报销单的行项目由财务自定义但申请人只能填金额和事由部门负责人只能批“同意/不同意”其他单元格包括计算逻辑、历史记录全部只读。这种权限不是“整张表只读”而是同一张表里不同角色拥有不同区域的编辑权用Excel做只能靠分散发送和人工合并体验很糟。再比如在线考试场景里试题模板由出题人定义考生只能填写答案单元格仓库盘点场景里管理员只能填写实盘数量库存上限、规格型号这些字段都锁死订单登记场景里客户的收货信息可编辑但价格、优惠金额这些由后台算好的字段不能碰。你会发现所有类似需求的内核都是同一套逻辑模板结构由一方维护填写操作由另一方执行中间的权限边界由系统强控。Univer正好能把这张表的边界画得一清二楚。2.3 为什么用表格引擎而不是用表单很多产品经理一听到“填表”第一反应是做表单——input加label一行一个字段。但稍微复杂一点的业务比如一个采购申请单要支持多行明细、合计金额自动计算、备注可多可少用表单做交互和布局会非常别扭。而表格天生适合这种结构行是记录列是字段公式自动算合计模板本身还可以被业务方随时调整。Univer给的正是这种自由度。它保留了Excel的网格模型用户可以像平时用Excel一样操作业务方也容易接受。相比之下表单方案的字段是写死的改一个字段样式要动前端代码表格方案则把“布局”交给了模板JSON改模板不需要动代码业务方自己就能在界面上做调整。这是表格引擎在填报场景里的最大优势。如果业务方说“我想在这个表里加一列备注”用表格引擎的方案对方只需要在模板编辑器里插入一列表单方案就得排期开发。3. 实操过程搭建Univer并实现“用户只能填指定单元格”3.1 环境准备与项目初始化先搭一个最基础的React项目用Vite脚手架即可。npm create vitelatest univer-demo -- --template react-ts cd univer-demo npm install然后安装Univer相关依赖。这里注意版本Univer的包版本升级比较频繁不同版本之间的API甚至包名都可能变化建议先到官方文档univer.ai或GitHub仓库确认当前稳定版本对应的安装命令。npm install univerjs/core univerjs/ui univerjs/sheets univerjs/sheets-ui npm install univerjs/engine-render univerjs/engine-formula npm install univerjs/design univerjs/network univerjs/threads如果环境是Vite React基本上这些包就够了。Univer内部模块很多我们只需要表格模块不引入文档和幻灯片模块可以减小打包体积。我实际做下来只引入表格相关插件时首屏加载和交互流畅度都还能接受。不要嫌装包这一步麻烦依赖关系理清楚后面能省很多排查时间。3.2 初始化Univer实例在入口文件里初始化Univer核心并注册渲染引擎、公式引擎、表格插件和UI插件。import { Univer, UniverInstanceType } from univerjs/core import { defaultTheme } from univerjs/design import { UniverRenderEngine } from univerjs/engine-render import { UniverFormulaEngine } from univerjs/engine-formula import { UniverSheetsPlugin } from univerjs/sheets import { UniverSheetsUIPlugin } from univerjs/sheets-ui import { UniverUIPlugin } from univerjs/ui const univer new Univer({ theme: defaultTheme, locale: zhCN, }) univer.registerPlugin(UniverRenderEngine) univer.registerPlugin(UniverFormulaEngine) univer.registerPlugin(UniverSheetsPlugin) univer.registerPlugin(UniverUIPlugin) univer.registerPlugin(UniverSheetsUIPlugin) const workbookData { id: workbook-1, name: 模板表格, sheets: {}, } const workbook univer.createUnit(UniverInstanceType.UNIVER_SHEET, workbookData)如果你的Univer版本较旧创建实例也可能写作univer.createWorkbook(workbookData)新版统一走createUnit。Univer的API确实变动很快我第一次写demo时也遇到过createUnit/createWorkbook的困惑。哪怕同一个大版本里小版本也可能有差异。我的建议是直接以官方文档为准GitHub仓库里有最小示例可以直接跑先不用纠结API。模板数据一旦被组件接受后面的操作都是围绕命令服务来做的即使API名称不同思路不会被推翻。3.3 设计模板预设表头、公式、样式“模板”在Univer里的本质就是一个符合某套工作簿数据结构的JSON。业务方通过界面编排的每一项改动最终都会变成这个JSON的变化。所以模板设计要做的就是把表头、样式、合并单元格、公式等预先写进这份数据里。const workbookData { id: workbook-1, name: 员工信息填报模板, sheetOrder: [sheet1], sheets: { sheet1: { id: sheet1, name: 填报页, rowCount: 20, columnCount: 6, cellData: { 0: { 0: { v: 员工信息登记表, s: { font: { bl: 1, sz: 14 }, fill: { bg: #f2f2f2 } } }, }, 2: { 0: { v: 姓名, s: { font: { bl: 1 } } }, 1: { v: 部门, s: { font: { bl: 1 } } }, 2: { v: 入职日期, s: { font: { bl: 1 } } }, 3: { v: 岗位, s: { font: { bl: 1 } } }, }, 3: { 0: { v: , locked: false }, 1: { v: , locked: false }, 2: { v: , locked: false }, 3: { v: , locked: false }, }, }, mergeData: [ { startRow: 0, endRow: 0, startColumn: 0, endColumn: 5 }, ], rowData: { 0: { h: 36, customHeight: true }, }, columnData: { 0: { w: 120, customWidth: true }, }, }, }, }看到这里面有个关键字段locked: false这就是后面做权限控制的核心。我在模板数据里已经把第3行的四个单元格标记为“可编辑”其余单元格保持默认锁定状态。这里给的JSON是简化过的示例实际Univer的字段格式里单元格对象除了v值和s样式公式单元格还需要写f公式属性不同类型的数据可能有不同的类型标记。总之模板的最终形态一定是JSON数据理解这一点模板设计就当成“写数据”来看而不是“写页面”。你在官方文档里找到对应的类型定义照着填就行。3.4 实现核心权限单元格锁定与工作表保护现在到了最关键的一步让用户只能填指定区域。Univer的权限模型跟Excel一样是双层结构。第一层是单元格的locked属性。它只是一个待生效标记如果工作表没有开启保护这个标记不生效所有单元格都可以编辑。第二层是工作表保护开关。一旦开启保护所有locked: true的单元格都会被禁止编辑只有locked: false的单元格可以正常输入。所以实现步骤是先把目标区域的单元格设为locked: false再开启工作表保护。那段模板JSON里已经把第3行的四个格子标成了可编辑下面用代码把保护开关打开。import { ICommandService } from univerjs/core import { SetWorksheetProtectionCommand } from univerjs/sheets const commandService univer.getInstance(ICommandService) await commandService.executeCommand(SetWorksheetProtectionCommand.id, { unitId: workbook-1, subUnitId: sheet1, protection: { options: { allowSelectLockedCells: false, allowSelectUnlockedCells: true, }, }, })执行完这行命令后界面上的效果非常明显用户点击锁定的单元格时光标会变成禁止操作的状态输入被拦截而点击刚才标记为locked: false的格子则可以正常输入文字、数字和公式。这个保护面板里还有不少选项比如是否允许选中锁定单元格、是否允许插入行列、是否允许排序筛选按业务需求勾选就行。如果你希望更严格一些还能给保护加密码防止有人手动取消保护。提示如果执行了保护命令但看起来没有生效先检查是不是目标单元格的locked属性没有正确设置为false。这是最容易踩的坑我后面会专门展开。这里还要提一点如果是动态创建模板模板数据更新后需要重新执行一次保护命令。因为工作簿重新加载时保护状态不会自动恢复。如果你在后台换了模板用户不刷新页面看到的还是老模板的老权限状态。尤其在做模板发布功能时一定要设计一个“发布成功后重新加载工作簿数据并重置保护”的流程。我早期版本就漏过这一步结果新模板上线后用户还能编辑已经被业务方锁掉的旧区域排查了半天才发现是保护状态没有跟着模板一起刷新。3.5 填表体验优化提示、校验、下拉选项只做到“能填不能填”还不够用户填表时还需要更友好的提示。建议在模板里加这么几样东西。第一把辅助列隐藏掉。有些字段是给公式用的用户不该看到也不该碰直接把列宽设为0或隐藏即可。第二用批注说明填写规则。在可编辑单元格的批注里写明“这里填数字最多两位小数”减少无效输入。第三配置数据验证给某些列设置下拉选项。Univer支持数据验证你可以给指定区域配置列表校验用户点开单元格就能看到下拉选择从源头上避免脏数据。import { AddWorksheetDataValidationCommand } from univerjs/sheets await commandService.executeCommand(AddWorksheetDataValidationCommand.id, { unitId: workbook-1, subUnitId: sheet1, rule: { type: list, allowBlank: true, formula1: 在职,离职,实习, ranges: [ { startRow: 3, startColumn: 1, endRow: 10, endColumn: 1 }, ], }, })这段代码给“部门状态”之类的一列配置了一个列表校验。用户填表时只能选择“在职/离职/实习”而不是自由输入。这里我以“部门状态”字段举例实际你可以换成任意一个需要固定选项的字段。Univer的数据验证类型不止列表一种还有数值范围、长度、日期等。我的建议是把所有涉及到选项、阈值、格式的规则都在模板JSON里约定好这样模板既承担了展示职责也承担了业务校验职责。用户看到下拉出现的那一刻就知道这个格子是系统强管控的。数据验证这块还会和后面的提交校验联动前端拦一次后端再拦一次。3.6 数据回收与导出用户填完之后数据怎么收这是三个层面里的数据层问题。有两种主流做法。第一种是监听Univer的命令执行事件记录用户改了哪些单元格把修改操作diff实时或批量回传给你自己的后端。代码长这样const commandService univer.getInstance(ICommandService) commandService.onCommandExecuted((command) { if (command.id SetRangeValuesCommand.id) { // 用户修改了某些单元格command.params 里有 range 和 value 信息 // 这里做你自己的 diff 记录比如上报 row/column 和值 } })第二种更直接在表单提交时把整张表或指定区域的值一次性拉出来。const sheet univer.getCurrentUnit(UniverInstanceType.UNIVER_SHEET)?.getActiveSheet() if (sheet) { const wholeValues sheet.getRange(0, 0, sheet.getRowCount(), sheet.getColumnCount()).getValue() // 把 wholeValues 发给后端保存 }我自己的习惯是两者结合后端保留模板JSON作为“答案纸”前端只提交用户在可编辑区域内的修改提交前再用模板JSON的结构做一次校验确保用户没有写入不该写的内容。这样即使有人绕过前端直接改数据后端也会拒绝。另外如果你只是需要表格的最终结果做归档Univer也支持把工作簿快照导出成类似Excel格式的文件直接交给业务方留档也行。4. 实战中的坑与排查技巧4.1 单元格锁定“失灵”先检查这三个地方我在落地时遇到过几次保护不生效的情况复盘下来基本都是三个原因。第一忘了把目标区域设置成locked: false。因为单元格默认就是锁定状态如果你只开保护、没解锁目标区域结果是所有格子都不能填用户会以为系统坏了。第二保护选项里勾了“允许选中锁定单元格”。这样用户仍然可以点击锁定区域虽然输入会被拦截但视觉上他会觉得这个格子“好像可以编辑”产生困惑。第三异步时序问题。Univer的命令执行是异步的如果你在onCommandExecuted里立即读取保护状态或者继续执行后续命令可能拿到旧状态。正确做法是等命令Promise resolve之后再继续。这几个问题其实都不是Univer的bug而是工作表保护机制本身的特性提前闹明白能少掉不少头发。4.2 模板版本与数据迁移业务方会时不时改模板这是必然的。我的做法是把模板和工作簿数据分开存储模板的每次修改记录为版本。发布新版本后旧数据仍然留在旧版本里或者做字段映射后迁移到新模板。Univer本身不会替你管理版本这些要放在你自己的服务端设计里。在线表格的cellData结构是JSON字段升级相对方便但仍然逃不过“老数据如何适配新模板”这个问题。每次发布模板时我建议保留一份snapshot快照发版后跑一次数据校验把历史数据里那些在新模板中已变成只读区域的字段做一次归档处理避免数据悄悄丢失。在我做过的项目里业务方改模板的频率比我预想的高得多所以这部分的投入一定不能省。4.3 协作模式下的权限边界如果用了Yjs协同编辑要注意工作表的保护功能是“UI层面的限制”它拦截的是普通用户的编辑操作并不等于数据层的硬权限。换句话说一个懂行的人如果绕过前端直接向协作后端发送命令理论上是有可能破坏只读区域的。所以只要场景里涉及重要数据我都建议把单元格级权限在服务端再校验一次。简单做法是为每个用户下发“可编辑范围列表”后端在处理同步进来的操作时过滤掉范围之外的写入。前端锁定是为了体验后端校验才是安全底线。这两件事不能互相替代。4.4 常见问题速查表把上线以来遇到的问题整理成一张表方便你快速对照。现象可能原因排查/解决思路用户完全不能输入所有单元格locked且没有解锁目标区域检查模板JSON中目标区域的单元格属性用户能选中锁定单元格但输入无效保护选项里允许了选中锁定单元格把allowSelectLockedCells设为false修改无效但控制台没有报错命令执行失败工作表处于保护状态打印command执行结果检查保护参数是否合法模板加载慢单元格总数太多、样式和数据量过大减少总行列数把不用的区域直接删掉按需渲染公式不重新计算未注册FormulaEngine或计算模式为手动确认注册了公式引擎检查自动重算设置多人编辑时锁定区域被改动前端锁定不是数据层安全服务端按可编辑范围过滤写入操作这张表不是万能的但覆盖了我踩过的绝大多数坑。遇到新问题的时候我会先打开Univer的控制台看命令执行记录很多现象都能在日志里找到根因。5. 落地后的几点经验与扩展玩法5.1 最值得投入的是模板治理不是写代码如果你打算用Univer做类似的填报系统我发现最花时间的其实不是Univer本身而是把业务模板梳理成“可被机器理解的表格”。哪些区域是固定说明、哪些区域是动态行、哪些单元格需要公式、哪些地方允许用户插入行这些一开始就要和业务方对齐。Univer的API允许你自由控制但自由意味着你必须先给业务方立好规矩。建议给每个可编辑区域定义一个明确的名称例如“员工信息填写区”既方便代码里做区域白名单也方便和业务沟通。模板治理做得越清晰后面的权限控制和数据回收就越省心。我在实际项目里光整理模板字段和业务规则就花了两周真正写Univer代码只花了三天。5.2 把Univer当基础设施还能延伸几个方向这套能力可以做很多扩展。你可以在模板里写隐藏辅助列用公式自动计算用户的填写质量可以用Univer的公式引擎在服务端跑一遍复算所有单元格结果防止用户提交伪造的合计值还可以把Univer当成一个轻量级报表设计器让业务方通过拖拽生成模板再把这些模板数据映射到填报页面。我之前提过Univer的公式引擎可以脱离UI在Node环境运行这对做数据校验非常有用。用户在网页上填了一个伪造的总金额前端看起来没问题但如果你在后端用同一套公式重新算一遍立刻就能发现不匹配把作弊的成本抬上去。这种“表格即逻辑”的用法比单纯在页面上画一堆输入框要优雅得多。5.3 最后分享一个实践技巧最后说一个我自己养成的习惯上线前把所有锁定单元格的颜色调成灰色底纹把可编辑区域保留白底。用户不需要任何说明看到灰色就知道不能动看到白色就知道该填。这种视觉上的引导比任何文档都有效。要改样式也很简单Univer支持通过样式接口批量修改区域底纹注册一条命令几行代码就能搞定。我始终觉得在线表格工具最大的价值不在于它多像Excel而在于它把一个原本靠人肉沟通的流程变成了可执行、可校验、可追溯的数据管道。Univer帮我把表格引擎这块省了下来但真正的产品逻辑——模板怎么配、权限怎么分、数据怎么收——仍然需要你自己想清楚。如果你正在调研同类需求不妨直接下一个Univer的Demo把一张日常使用的Excel模板搬上去先试试“锁定单元格”这个功能你会发现最普通的场景里藏着不少细节。等你能自如控制单元格的读写状态你会意识到这已经不只是一个填表工具而是一层可以承载业务流程的基础设施。
返回列表