尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

用Univer实现可编辑/只读混合表格:区域权限+命令拦截实战

用Univer实现可编辑/只读混合表格:区域权限+命令拦截实战 我最近在做一个内部数据收集系统需求听起来不大管理员预先设计一张表格说明文字和表头都填好然后把某些空白格子开放给用户填写其余所有单元格都不可修改。放在Excel里就是“保护工作表部分区域解锁”但放到浏览器里还要嵌入公司现有系统事情就变得没那么简单了。我先后试过用HTML table硬搓、用过几款在线表格组件最后在一个开源项目上稳定住了——这就是Univer。Univer是一个用TypeScript写的开源办公套件基础设施主打在线电子表格后续也覆盖文档和幻灯片渲染引擎是自研的UI、命令系统、协作能力都可以通过插件按需加载。这篇博文是我把它真正跑通到生产环境的记录包括如何初始化、如何实现“指定单元格可编辑、其他区域只读”的核心需求以及过程中遇到的那些文档里基本不会写的坑。如果你正好也在做表格收集、任务分配、报价单在线填写这类需求这篇文章应该能帮你少走不少弯路。1. 为什么是Univer而不是自研表格或传统表单工具1.1 这类需求到底在要什么这个场景很常见我把它叫“受限填写表”管理员先定义好一张表的骨架——表头、字段说明、汇总公式、填写提示然后把其中少数空白区域标记为“可编辑”。用户打开这张表时只能在这些区域里录入内容其他地方无论是点击、粘贴还是拖拽填充都应该没有反应。最开始我尝试过传统表单工具像问卷类产品。它们的表单控件很成熟但问题是业务要保持“Excel式”的布局字段之间有大量合并单元格、备注、公式联动控件型表单根本表达不了。后来想到直接用在线表格数据在别人服务器上API粒度大多停在“整表导出”想做区域级权限控制非常别扭。再后来我在GitHub上找到了几款纯前端电子表格组件功能不错但维护状态让人不安新浏览器兼容性、TypeScript支持、现代框架集成都处于一个不太放心的状态。最后锁定Univer有四个原因第一它是面向“二次开发”的表格引擎核心数据模型抽象得很干净单元格、样式、范围、工作簿都是对象化API第二它用命令模式管理所有操作修改单元格、设置样式、合并区域在内部都是命令这就给权限拦截留了天然的钩子第三官方在持续迭代社区活跃度在开源表格里可以说排在前列第四它对现代前端栈友好基于TypeScriptRxJS方便在我们现有的Vue/React体系里做深度集成。1.2 Univer与在线表格服务、传统表单的差异这里放个对比可能更直观方案类型代表适合场景区域权限控制可嵌入业务系统数据私有化在线表格服务Google Sheets、飞书表格纯协作、轻编辑场景基本只能整表/按人设权限受平台API限制数据在第三方表单工具问卷星、表单类SaaS字段固定的采集场景无表格概念只有控件一般有嵌入iframe依赖平台自研HTML表格自己写table展示型数据列表完全靠自己完全可控完全可控开源表格引擎Univer、Luckysheet等需要表格/公式/样式并嵌入自己系统的场景可通过权限API命令拦截实现完全可控完全可控从这个表能看出Univer这套方案的真正价值在于“中间态”它把接近Excel的编辑能力开放给你同时把代码控制权全部交还给你。你可以把它的工具栏禁用可以监听每一个命令可以在命令执行前插入自己的校验逻辑也可以把最终数据用任意协议送到任意后端。1.3 Univer当前能提供的核心能力边界我实际用到和关注到的能力大致包括单元格编辑器与富文本输入支持快捷键、复制粘贴、拖拽填充、撤销重做公式计算引擎常用函数和新版本兼容做得不错跟Excel的差异在官方文档里有对照表单元格样式包括背景、字体、边框、对齐、数字格式、条件格式数据验证支持下拉选择、数值范围、自定义验证筛选、排序以及后续的图表、数据透视表等插件导出xlsx权限系统覆盖工作簿、工作表、区域三级协同能力官方推的是集成Yjs做数据同步我们因为场景简单暂时没有上。实话实说Univer的版本迭代比较快有些插件比如协同、导出、打印会标注“experimental”或“beta”API也偶尔有破坏性变更。所以做生产项目时第一件事是把版本锁死第二件事是准备一份“插件能力白名单”不要所有功能都塞进去把用不到的功能当冗余删掉能减少很多兼容性麻烦。2. 用Univer搭一个可编辑/只读混合表格整体流程与项目骨架2.1 初始化项目与插件注册先看安装。我用的是pnpmnpm也同样。npm install univerjs/core univerjs/sheets univerjs/sheets-ui univerjs/ui univerjs/design univerjs/engine-render这些包里core是数据模型和命令总线sheets是电子表格的逻辑层sheets-ui负责表格交互和渲染ui提供工具栏、菜单、弹窗这些外围组件design是设计系统engine-render是自研渲染引擎。它们之间分层很明确core不依赖DOMsheets可以在无头环境跑这其实也是Univer适合做二次开发的重要原因——你甚至可以自己写一套UI只复用底层计算和命令。初始化代码大体是这样import { Univer, UniverInstanceType } from univerjs/core; import { defaultTheme } from univerjs/design; import { UniverSheetsPlugin } from univerjs/sheets; import { UniverSheetsUIPlugin } from univerjs/sheets-ui; import { UniverUIPlugin } from univerjs/ui; const univer new Univer({ theme: defaultTheme }); univer.registerPlugin(UniverSheetsPlugin); univer.registerPlugin(UniverUIPlugin, { container: app, }); univer.registerPlugin(UniverSheetsUIPlugin);注意一点不同版本对container的传递方式不太一样有的版本在UniverUIPlugin里传有的版本在SheetsUIPlugin里传。如果你安装后界面没出来第一步去查这个参数的位置而不是去查CSS。HTML里需要一个带高度的容器div idapp stylewidth: 100%; height: 600px/div表格容器如果没有高度渲染引擎会得到一个0高度的矩形表现就是整个画布空白。这个坑很常见先提一嘴。2.2 创建带模板数据的WorkbookUniver通过createUnit创建工作簿传入的数据结构是IWorkbookData。我来做一个“员工信息登记表”第1行是表头A列是字段名B列是填写内容C列是提示。const workbook univer.createUnit(UniverInstanceType.UNIVER_SHEET, { name: 员工信息登记表, sheetOrder: [sheet-1], sheets: { sheet-1: { id: sheet-1, name: 登记表, rowCount: 100, columnCount: 8, cellData: { 0: { 0: { v: 字段 }, 1: { v: 填写内容 }, 2: { v: 填写说明 }, }, 1: { 0: { v: 姓名 }, 1: { v: }, 2: { v: 请填写真实姓名 }, }, 2: { 0: { v: 部门 }, 1: { v: }, 2: { v: 下拉选择 }, }, // 更多行... }, }, }, });cellData的索引是行、列从0开始。值v可以是字符串、数字、富文本对象样式通过s字段设置。比如我想把表头加上深色背景{ 0: { 0: { v: 字段, s: { backgroundColor: #D9E1F2, fontWeight: bold } }, } }如果只是做模板手写JSON没问题但如果字段很多建议让后端统一输出这个结构前端直接透传。这也是Univer适合做“用户自定义表格”的原因整套表格定义本质上是可序列化的JSON数据库里存一份用户打开时渲染出来管理员修改时也是改这份JSON。2.3 明确可编辑区域样式与提示的前期设计模板建好之后先别急着写权限代码。我建议先把“可编辑区域”和“只读区域”在视觉上明确区分开可编辑区域用浅黄色底、加细边框只读区域用灰色底。这样用户在视觉上就知道哪里能填再配合权限控制体验会好很多。用Facade API写起来比较直观import { univerAPI } from univerjs/core; const activeWorkbook univerAPI.getActiveWorkbook(); const sheet activeWorkbook?.getActiveSheet(); // 先给整表加灰色底最终大部分区域都会被锁住 sheet?.getRange(0, 0, 100, 8).setStyle({ backgroundColor: #F7F7F7, lock: true, }); // 再给填写区域覆盖黄色底 sheet?.getRange(1, 1, 20, 1).setStyle({ backgroundColor: #FFF8E1, lock: false, });如果你的版本没有全局暴露univerAPI可以去 univerjs/facade 里找对应的导出或者直接用底层Sheet对象。这里的lock属性先埋个伏笔在Excel里单元格锁定只在“工作表保护”开启时才生效。Univer的模型也类似所以样式只是第一步真正的约束要靠权限和命令拦截下一章细讲。还可以在可编辑区域下面加一行提示“黄色区域可填写灰色区域为只读”。如果担心用户误操作可以在工具栏配置里把不相关的编辑功能藏起来。3. 真正实现“只有指定单元格可填写”的几种路径3.1 工作簿权限与工作表权限的基础用法Univer在底层有一层权限系统设计上参考了企业级办公的常见模型权限作用在“实体”上实体分工作簿、工作表、区域三个层级每一层可以配置允许或禁止的操作。最粗的用法是把整张工作表设置为只读所有用户只能看不能改。这种场景比如用表格做报表展示、做只读的看板数据。实现方式是通过命令执行一次“设置工作表权限”的Mutation。这个权限配置也存在workbook的resources里可以随工作簿数据一起持久化。具体Mutation的名字在不同版本里略有变化我这里给个示意// 示意通过命令服务执行权限变更 await commandService.executeCommand({ id: sheet.mutation.set-worksheet-permission, params: { unitId: workbook-1, sheetId: sheet-1, permission: { // 把编辑类权限全部设为 false editCell: false, editStyle: false, }, }, });由于不同版本的ID和参数结构差异较大写这种代码前去你安装的node_modules/univerjs/sheets里翻一下mutation定义或者直接查官方权限demo比自己猜要靠谱得多。我把这一步确认API的方式当成经验讲因为好多人一上来抄老代码结果API对不上卡一下午。3.2 按区域锁定RangePermission实现精细化管控如果只是整表只读那用表单展示就行。真正的难点在“部分可编辑”这时要用的就是区域权限。思路是把整张表先设置为“无编辑权限”然后只对某些range开放写权限。在Excel里等价于“保护工作表然后把填写区域从锁定中排除”。区域权限可以用类似的方式下发// 示意对指定 range 设置可编辑权限 await commandService.executeCommand({ id: sheet.mutation.set-range-permission, params: { unitId: workbook-1, sheetId: sheet-1, ranges: [ { startRow: 1, endRow: 20, startCol: 1, endCol: 1 }, { startRow: 1, endRow: 20, startCol: 3, endCol: 4 }, ], permission: { editCell: true, editStyle: false }, }, });这样用户只能在B列和D到E列的对应区域里输入文字其他区域点上去不会出现编辑光标。这个能力是Univer权限模型天然支持的比自己在UI上做“假锁定”要扎实得多——因为命令系统在数据层就拒绝了写入。需要提一点权限配置要跟随工作簿数据保存。Univer工作簿里会带resources字段里面可以存自定义资源。我建议把权限配置也序列化到resources里重新打开表格时权限自动恢复而不是每次启动都手动执行一遍命令。3.3 命令拦截兜底防止绕过UI层操作区域权限基本能覆盖日常操作但在复杂场景下我还是建议再加一道“命令拦截”原因有三个用户可以通过粘贴、拖拽填充、从其他sheet复制等方式一次性触发多个区域的写入权限系统的校验逻辑未必覆盖到你想要的所有操作类型如果你后续接了协同、外部同步权限配置可能需要按用户、按会话动态判断这时候只靠静态的区域权限配置不够灵活你可能会想做审计谁、什么时候、改了哪些单元格。命令拦截的思路很简单Univer里所有修改单元格内容的操作最终都会走到几个Mutation比如SetRangeValuesMutation改值、SetRangeStyleMutation改样式、InsertRowMutation插入行。我们在命令执行前订阅一个事件检查这次改动的range是否落在白名单里不在就拒绝。示意代码import { SetRangeValuesMutation } from univerjs/sheets; import type { ICommandService } from univerjs/core; const editableRanges [ { startRow: 1, endRow: 20, startCol: 1, endCol: 1 }, ]; function isEditable(range: { startRow: number; endRow: number; startCol: number; endCol: number }) { return editableRanges.some((r) ( range.startRow r.startRow range.endRow r.endRow range.startCol r.startCol range.endCol r.endCol )); } commandService.beforeCommandExecute$.subscribe((command) { if (command.id ! SetRangeValuesMutation.id) return; const params command.params as { range?: { startRow: number; endRow: number; startCol: number; endCol: number } }; if (!params?.range || !isEditable(params.range)) { // 阻止本次修改并提示用户 console.warn(该区域不可编辑, params?.range); return false; // 具体返回值语义以版本文档为准 } return true; });这段代码是一个“示意”真正落地时你还要处理一个很现实的问题命令系统里有内部命令和用户命令之分。Univer的undo、redo、协同同步都会复用这些Mutation如果你不分来源直接拦截可能连撤销操作都被拦掉用户会非常崩溃。我的做法是在命令参数里加一个来源标记只拦截来源为“用户编辑”的命令对内部同步命令放行。3.4 三种方案的取舍与组合策略把三种路径总结一下方案粒度优点缺点适合场景工作表级权限整表配置简单、性能好无法区分区域内可编辑只读报表、展示型表格区域权限单元格范围粒度细、官方支持API版本变化快、需要持久化配置模板填报、指定单元格填写命令拦截命令级完全可控、可审计、可动态判断开发量大、需要处理内部命令白名单复杂业务规则、协同场景我的推荐组合是“区域权限为主命令拦截兜底”。区域权限负责最常用的限制命令拦截负责处理边缘情况和业务联动。比如当用户试图编辑一个被锁定的合并单元格时区域权限可能只拦住了其中某个单元格但命令拦截会把整个合并区域的range都拉出来判断把漏网的拦截掉。两层配合下来基本能做到用户想改也改不了。4. 实际落地时躲不开的坑4.1 样式锁定与内容编辑是两回事这是我在第一个demo里踩的坑我给不需要编辑的单元格设置了lock: true然后高兴地以为搞定了结果用户在界面上照样能打字。后来查文档才意识到Univer跟Excel一样单元格的lock只是一个“样式标记”真正让锁定生效的是工作表的保护状态或权限判断。只有lock: true但保护层没有启用锁等于摆设。所以落地顺序应该是先定义好可编辑区域再设置区域权限最后用样式把可编辑区域视觉化。三条线缺一条都不完整。4.2 公式区域、合并单元格与动态行数第二个坑来自公式。我在模板底部加了一行合计公式SUM(B2:B20)但没有把公式区域加入只读保护结果测试时有人不小心在公式单元格里粘贴了一个值公式被覆盖整张表的统计结果直接变成0。这个教训让我养成了一个习惯所有含公式的单元格无论看起来多普通都必须在权限层锁死。合并单元格也有类似的问题。Univer里一个合并区域在权限模型里通常会被视作一个整体你在A1到A2上做了合并用户点击A1时选区range可能直接变成A1到A2。如果只锁了A1没锁A2拦截逻辑就可能出现漏判。我的方案是在计算白名单时先把所有合并区域解析出来然后统一按“合并区域”的单位来判断是否可编辑。还有一个业务层面的问题用户填报时可能需要动态增加行数。如果模板固定了rowCount: 100填到最后没有空行了就要允许“插入行”但插入行又会改变后续所有单元格的坐标。这个我在项目里交给了后端处理前端预留足够行数不做动态插入文件提交后再由后端裁剪空行避免把权限和坐标搞复杂。4.3 并发协作场景下权限控制的失效风险如果你的表格是多人同时在线编辑事情会比单人场景复杂得多。Univer本身支持基于CRDT的协同方案典型是Yjs但权限判断如果只停留在前端风险很大用户A通过某个内部接口直接改了某单元格协同同步到用户B时B端的前端校验根本拦不住这次改动。因为这条同步消息经过的不是用户编辑命令而是数据同步通道。所以一旦上协同“区域只读”就不能只靠前端。需要做两件事一是服务端在每条同步消息落库之前校验操作者的权限二是客户端把权限配置也同步给每个会话让UI层的禁用状态一致。我自己的做法是在协同方案里给每条改动消息带上操作者身份服务端持有一份“用户到可编辑区域”的映射表每次消息进库前校验。这套逻辑其实已经脱离了Univer本身属于你的业务系统但一定要提前设计否则后面补会很疼。4.4 导出与隐藏公式的配套处理用户填完表管理员需要导出Excel。这里有两个细节要注意。第一Univer的导出插件目前侧重单元格数据和样式权限配置、保护状态这类“应用层信息”不一定能完整写进xlsx。导出的文件在Excel里打开后那些灰色只读区域可能就是普通单元格任何人都能改。如果你给用户的交付物要求“模板锁定”要用exceljs这样的库在导出文件上补一遍sheet protection把哪些列锁定、哪些列解锁重新做一遍。第二公式单元格的隐藏。Excel里有个“隐藏公式”的选项开启后用户看不到公式内容这个行为同样依赖工作表保护。Univer里是否完整复刻、以及导出时是否保留都要实测。我的经验是公式隐藏和单元格锁定都还没有默认导出到你想要的xlsx效果必须自己后处理。最后再补一个跟业务相关的坑用户提交数据时前端拿到的是单元格值但如果你保存了单元格样式、合并信息重新打开才能还原布局。所以提交接口最好同时传values和merges、styles这些信息否则就会出现“数据对了表格版式全乱”的尴尬场面。5. 这套方案还能延伸到哪些场景5.1 数据收集、报价、审批流“受限填写表”这个需求不止是我做的数据收集系统它也是很多业务系统的通用组件。比如供应商报价采购方在表格里设定产品单价、交期等字段开放给供应商填写其他区域比如历史报价、内部议价信息全部锁死。再比如审批流里的月度计划表计划目标由上级填写执行结果由下级填写同一张表不同区域给不同角色开放。用Univer加上后端动态权限配置这类需求可以实现得非常优雅而且用户不用离开你的业务系统界面。5.2 嵌入到已有系统的权限对接如果你已经有一套比较成熟的后端权限体系可以把可编辑区域的设计完全交给后端。管理员在后台配置页面上“画”一个区域然后把配置存成JSON{ workbookId: workbook-1, editableRegions: [ { sheetId: sheet-1, startRow: 1, endRow: 20, startCol: 1, endCol: 1, roles: [user] }, { sheetId: sheet-1, startRow: 1, endRow: 20, startCol: 3, endCol: 3, roles: [admin] } ] }前端在初始化Univer时拉取这个配置转成权限命令和拦截白名单后端在接收提交数据时也按同一份JSON校验。一套配置两个端共用权限逻辑保持一致以后改起来也只改数据库里的配置不用动代码。5.3 与后端接口联动的保存策略保存策略可以根据需求灵活选手动提交用户填完点按钮前端调用getActiveSheet().getRange().getValues()这类接口取走数据打包POST到后端。适合问卷、报名、报价提交这类流程性场景自动保存监听Univer的编辑命令每次改动后防抖同步到后端。适合“边写边存”的协作场景协同保存基于Yjs等方案做实时同步所有操作通过协同通道分发。适合多人同时编辑同一张表的场景。在这三种策略里命令拦截扮演的角色不太一样。手动提交场景最省事权限只要在提交时校验一次自动保存场景则要在每条改动进库前校验协同场景最复杂需要服务端层级的校验。我的建议是如果你的业务不是强协作不要轻易引入协同方案因为它的权限边界、冲突处理、服务端实现都会把项目复杂度抬高一个量级。做完这个项目我最大的体会是Univer这类开源表格引擎真正的价值不是“像不像Excel”而是它把所有表格操作都抽象成了可以被监听、被拦截、被序列化的对象和命令。这种设计让“指定单元格可编辑其他区域不可改”这种需求不再需要靠CSS遮罩、disabled输入框这类hack去实现而是可以在数据层和命令层做约束。最后分享一个小技巧如果你打算长期用它建议在项目里给Univer封装一个薄薄的Adapter层把创建工作簿、设置权限、提交数据这几个操作集中封装。内部无论API怎么变只需要改Adapter一个文件。我做这个项目时Univer的API就在我使用的过程中升级过两次得益于这层封装业务代码几乎没有改动。这个习惯算是我这次踩坑换来的最实用的一条经验。
返回列表