ARTICLE DETAIL

资讯详情

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

Univer工作表保护实战:实现只读单元格与可编辑白名单

Univer工作表保护实战:实现只读单元格与可编辑白名单 如果你的业务系统需要在页面里嵌一张“只能填部分格子的表格”你多半会很快搜到“Univer”这个词。Univer是最近热度很高的一款开源在线表格引擎基于Canvas自研渲染器TypeScript全栈实现既能当完整编辑器用也能抠出来当成数据录入组件。我最近刚好在一个内部系统里做预算填报模块需求很经典表格模板由管理员维护填报人只能改指定的白色单元格其他有公式、有表头的区域一律只读最好连右键菜单里的编辑项都消失。折腾完Univer后我发现这个需求本身不难但网上资料大多停留在“安装一个表格编辑器”真正讲到保护机制、权限模型和防绕过的干货很少。这篇文章把我踩过的坑和最终方案整理出来给正打算接入Univer做填报、审批、信息收集类功能的同学参考。1. 先弄清楚“不可编辑”的真实含义再做方案1.1 为什么“锁单元格”看起来简单做起来容易翻车做项目时经常遇到这种情况需求方说“用户填下这几个格子就行”听起来就像在单元格上写个readonly属性。但等你真动手问题全冒出来了表头能不能改行能不能加公式被误删了怎么办合并单元格要不要保护下拉框、批注、数据校验还生效吗我把这称作“没说出口的需求清单”。在动手之前至少要跟业务方对齐五件事哪些单元格区域允许编辑哪些仅展示是否允许插入行/列、删除行/列、拖动填充被保护区域是“完全不响应鼠标键盘”还是点击后弹出提示提交后的数据是否允许在表格里二次修改管理员和普通用户的权限边界怎么划分。很多团队第一步就走错了为了省事用一层contenteditable或者给整张页面加遮罩来实现“只读”。这种做法的致命伤在于编辑状态和数据模型是分裂的。用户一旦通过快捷键、拖拽、剪贴板批量操作前端根本拦不住就算拦住了撤销、重做、协同编辑这些基础能力也全部要从零做起。更别提数据校验和公式重算。所以我在这个项目里的结论是如果业务体量已经大到“填报表线上化”的程度不要自己造轮子直接用Univer这种有完整命令系统和数据模型的引擎然后靠它的权限框架做收敛。1.2 为什么我最终选择Univer而不是其他方案当时摆在面前的有三条路第一种用现成的在线表格产品做嵌入比如通过iframe引第三方表格。好处是几乎零开发坏处是身份体系、样式定制、权限控制全都受制于人而且用户管理要是没打通填报人的编辑范围就很难精确到单元格。第二种用前端表格控件比如AG Grid这类配合自定义renderer实现只读。AG Grid的虚拟滚动和编辑事件确实成熟但它本质是“表格组件”不是“电子表格”。公式、合并单元格、多级撤销、Excel粘贴格式这些能力要么没有要么做得很浅。如果需求只是展示表格它够用如果需求是“像Excel一样操作但某些格子不能动”它就不够舒服。第三种Univer。它是完整的多端电子表格引擎公式引擎、条件格式、数据透视都在路线图上而且渲染层是Canvas自绘大数据量下不会像DOM表格一样卡到不行。对我这个场景最友好的一点是Univer有一套和Excel语义对齐的保护机制工作表可以开启保护保护范围内还有“可编辑区域”白名单。我只需要把允许用户填写的那些单元格加进白名单剩下的区域天然只读。我当然也评估过它的缺点文档还在快速迭代版本之间API会有变化网上中文案例少。但这些属于“学习成本”不是“方案不可行”。更重要的是Univer的插件化架构让后面接协同编辑、接离线包、接自定义工具栏都留了口子这是其他前端表格控件给不了的。2. Univer里的保护与可编辑范围三张牌怎么打2.1 先从Excel的Locked语义说起很多人第一次接触Univer时会误以为单元格样式里既然有locked属性那我把它设成true用户就不能改了。这个理解只对了一半。实际上单元格样式里的locked只是“一把挂在门上的锁”真正决定能不能进门还要看工作表有没有“关门”——也就是保护开关是否打开。用Excel的原生逻辑来解释就清楚了单元格默认处于“锁定”状态但工作表默认未保护所以默认情况下所有单元格都能编辑当你开启了“保护工作表”所有locked为true的单元格就不可编辑而你在保护时勾选的“允许用户编辑区域”其实就是把某些区域的locked属性临时解除或者把区域加入白名单让它们在保护状态下仍然可填。Univer沿用了这一套语义。这带来一个很重要的认知“只读”不是一个单元格属性而是“工作表保护开关 单元格锁定属性 可编辑范围白名单”三者共同作用的结果。如果你一开始没搞清楚这个模型很容易出现两种反直觉的现象明明给单元格设置了locked: true工作表没开保护用户照样双击就能改或者工作表保护开了但忘了给可填写区域加白名单结果整张表谁都填不了。2.2 工作簿、工作表、区域三层权限的配合逻辑Univer的权限体系是分层的从宏观到微观一共三层我习惯叫它“三张牌”。第一层是工作簿保护。这一层管的是“结构性操作”比如能不能新增工作表、删除工作表、拖动工作表排序。如果只是想让用户填数据工作簿保护建议直接打开否则用户可能通过新增一个Sheet来绕开你精心设计的页面。第二层是工作表保护。这一层管“内容编辑”。开启了工作表保护才能让上面的locked属性真正生效。注意这里保护的是“这张Sheet里的内容”不是整个工作簿。如果Sheet很多你得确保需要限制的那张Sheet开了保护。第三层是区域级可编辑范围。这一层是白名单也是我们做“表格填写”需求时真正花心思的地方。你需要在保护状态下显式声明“哪些区域允许用户编辑”。比如A1:B10允许填写那么这块区域即使在保护状态下也保持可编辑而其余区域全部只读。这三层的关系是层层递进的工作簿保护管结构工作表保护管内容区域白名单管例外。用表格整理一下更直观层级管什么不打开会怎样工作簿保护新增/删除/重命名工作表用户可能加Sheet绕过限制工作表保护单元格内容、公式、对象编辑locked属性不生效可编辑范围保护状态下仍可填写的区域会出现“全部锁定”或“全部可编辑”理解完这三张牌再回头看需求“用户只能填指定单元格其他单元格无法修改”其实就是一句话打开第二层工作表保护然后在第三层把指定单元格加进可编辑范围白名单。方向一下子清晰了。3. 实操配置把Univer改造成“填报模板”的4个关键动作3.1 基础安装与插件初始化Univer的包结构比较细不是一个大杂烩包而是核心、表格引擎、表格UI、公式分开的。做填报场景我建议至少引入以下插件univerjs/core核心数据模型负责文档生命周期、命令系统、撤销栈univerjs/sheets电子表格引擎核心univerjs/sheets-ui表格UI层提供工具栏、右键菜单、渲染画布univerjs/sheets-formula公式引擎模板里如果有合计、汇总公式就必须装不装的话Excel里的公式即使显示了也不会计算。初始化的基本写法大致是这样不同版本包的导出名可能略有差异以你当前版本的类型声明为准import { Univer, UniverInstanceType } from univerjs/core; import { UniverSheetsPlugin } from univerjs/sheets; import { UniverSheetsUIPlugin } from univerjs/sheets-ui; import { UniverSheetsFormulaPlugin } from univerjs/sheets-formula; const univer new Univer({ locale: zhCN, plugins: [ UniverSheetsPlugin, UniverSheetsFormulaPlugin, UniverSheetsUIPlugin, ], }); // 创建一个包含模板数据的Sheet const workbook univer.createUnit( UniverInstanceType.SHEET, { id: budget-template, sheetOrder: [sheet1], sheets: [ { id: sheet1, name: 预算填报, rowCount: 50, columnCount: 20, cellData: { 0: { 0: { v: 项目名称 }, 1: { v: 预算科目 }, 2: { v: 金额 } }, // ...模板数据 }, }, ], } );这里有个容易被忽略的点如果你的模板是直接从Excel文件转成Univer数据格式的那在初始化时就要把单元格的样式、合并、锁定状态一起带进来。不要先建一个空白Sheet再通过JS去补样式那样代码会很碎而且容易漏。3.2 开启工作表保护并声明可编辑范围初始化完成之后真正关键的动作来了开保护、加白名单。Univer的SDK提供了高层API也可以通过命令服务派发底层Mutation。思路级示例代码如下// 获取当前Sheet的操作入口 const fUniver univer.__getAPI(); const workbook fUniver.getActiveWorkbook(); const sheet workbook?.getActiveSheet(); // 1. 开启工作表保护 sheet.setSheetProtection({ selectLockedCells: false, // 是否允许选择被锁定的单元格 selectUnlockedCells: true, // 是否允许选择未锁定的单元格 }); // 2. 把允许用户填写的区域加入“可编辑范围”白名单 sheet.addEditableRange({ title: 填报区域, range: { startRow: 3, // 从第4行开始索引从0计 endRow: 20, startColumn: 1, endColumn: 3, }, });如果你用的Univer版本没有暴露setSheetProtection、addEditableRange这样的高层方法也不用慌。Univer所有操作最终都通过命令系统走你可以用commandService.executeCommand直接派发“设置工作表保护”和“新增可编辑区域”的Mutation命令。具体方法名建议去你安装的包里翻一下类型定义比在官网大海捞针要快得多。开保护之后我还做了一次遍历检查把填充色为白色、且需要用户填写的单元格的样式里locked属性显式设为false尽管白名单已经生效显式设置能避免某些版本行为不一致。这一步是我在后面的踩坑里发现必须做的原因后面专门讲。3.3 收窄UI编辑入口和键盘行为权限模型只能保证“数据层”的编辑被拦截但用户界面上还是能看到右键菜单、工具栏按钮这些入口。如果点了没反应体验也很差。我当时在UI层面做了三件事第一自定义右键菜单。Univer的UI插件支持对菜单项做裁剪。把“删除行”“删除列”“插入行”“清除内容”“格式刷”这些和填报场景无关的菜单项隐藏掉。只保留“复制”“粘贴为纯文本”这类安全操作。第二设置工具栏按钮的可用状态。填报表不需要用户调字号、加边框所以工具栏上只保留了“撤销”“重做”“打印”和“下载模板”。其他按钮要么隐藏要么置灰。第三监听选中区域的变化。当用户点击到只读区域时状态栏显示“该区域受保护仅可查看”当用户点进可填充区域时显示“可编辑”。这纯属体验优化但对业务方来说感知非常强烈他们会觉得系统“有灵性”而不是干巴巴地拦截操作。这里补充一句UI裁剪只是让用户“看不见入口”权限真正生效靠的是保护机制本身。两者是配合关系不能互相替代。4. 用户可以想出一百种方法绕过限制这些坑我替你踩了4.1 复制粘贴最常见也最容易被忽略的绕过路径我上线第一版后收到的反馈很有意思“表格是锁住了但我把A1的表头复制一下粘贴到A5怎么也能贴上去”这就是典型的“绕过路径”。用户的CtrlC/CtrlV走的是剪贴板粘贴命令它跟直接双击进入单元格编辑不是同一条代码路径。单元格编辑有一个“进入编辑态”的校验而粘贴操作是批量写入很多表格引擎出于性能考虑在粘贴时只做区域存在性检查不做逐格权限校验。解决思路有两个方向。第一个方向在前端拦截粘贴事件。监听Univer命令系统里跟粘贴相关的命令取目标区域后和可编辑范围白名单做交集计算如果粘贴目标区域超出了白名单就中止命令并弹出提示。这个方案能挡住绝大多数普通用户。第二个方向在服务端/共享数据层做校验。前端拦截只防“误操作”防不了“恶意请求”。用户只要打开开发者工具可以直接调Univer的API绕过UI甚至根本不走前端直接往后端发数据。所以填报类的项目必须把校验放到后端或协同服务器这是后话第5章详细讲。我两个方向都做了。前端拦截保住体验后端校验守住数据底线。4.2 拖拽填充、删除行列与撤销重做结构性操作的连锁反应除了复制粘贴还有三个操作是保护机制的重灾区。第一个是拖拽填充。用户点中一个可编辑单元格右下角的黑色小方块往下拖拖进只读区域数据照样被填进去。因为拖拽填充在实现上是一段“连续区域写入”的命令它内部对目标区域的权限检查不同版本处理方式不太一致。我在项目里干脆把拖拽填充功能关掉了因为填报表里实在没有这个必要。第二个是删除行列。用户选中一行按删除键结果这一行无论是否在可编辑范围都可能被删掉导致后面的数据整体上移模板结构全乱。要解决这个问题我在两个层面下手一是通过权限配置禁止删除行列二是监听命令事件如果发现用户试图删除“包含只读单元格的行列”直接拦截。如果你做不到禁止删除也要在删除前做区域映射检查确认删除区域里没有受保护单元格。第三个是撤销和重做。这个坑比较隐蔽用户先在一个可编辑区域填了内容又转头去操作受保护区域这时候按CtrlZUniver的撤销栈可能会把“之前那次被保护区域写入操作”一并回滚。如果权限校验是在命令执行后才生效撤销时校验可能被跳过。我的做法是在关键流程里关闭表格的撤销栈或者每次保存/提交时重置撤销历史。填报表不需要那么强的撤销能力稳定优先。把这些绕过路径拉个清单方便对照操作风险等级处理思路复制粘贴到只读区域高前端拦截后端校验拖拽填充跨区域高关闭拖拽填充删除行列高禁止删除或拦截命令撤销/重做绕过校验中重置撤销历史通过控制台调API写入很高后端/协同层校验5. 真正可靠的填报方案从来不止是前端锁格子5.1 工程上的校验闭环前端限制不等于数据安全写到这里我必须说明一点Univer的所有锁定、保护、白名单本质上都是“前端规则”。前端规则能拦住普通用户但拦不住一个打开开发者工具的人。因为Univer跑在浏览器里所有命令、Mutation、数据写入理论上都可以被JS调用。所以做填报表正确的姿势是把前端锁定当作“第一道门”它的价值是提供符合直觉的交互边界用户看到灰色格子就知道不能动点击只读区域不会出现光标误操作会被善意提醒。这99%的场景已经解决了。剩下1%的恶意场景需要靠第二道门兜住以保存/提交为校验点。用户提交数据时前端把填写结果传到服务端服务端按照国家规范读一下模板定义校验“提交的数据区域是否都在允许白名单内”“值是否符合字段规则”再把校验通过的数据存库。这样设计还有一个额外好处模板如果改版了比如管理员把某列变成“禁止填写”那已经提交的旧数据可以保留新提交的数据必须遵循新规则。如果只依赖前端锁定模板一变用户浏览器里的旧版本可能还在按旧规则执行就会出现“前端让填后端不让存”的矛盾。5.2 一套更稳妥的填报模块设计踩完前面的坑之后我现在做Univer填报表的默认架构是这么设计的模板定义与实例数据分离。管理员在后台维护一张“模板Sheet”里面定义了哪些单元格可填、哪些只读、字段类型是什么。填报人打开页面时系统根据模板生成一个“实例Sheet”这个实例里的样式、合并、公式都从模板复制但数据留空。可编辑范围白名单也根据模板动态生成。这样管理人员不需要懂代码直接在表格里改模板就能调整填报区域。用“提交”而不是“实时保存”作为数据入口。用户在Univer里填的内容先停留在前端实例里点击“提交”时前端做一次快速校验通过后把数据发给后端后端拿到数据后根据模板重新锁定校验再决定是否入库。整个过程里Univer更像一个“输入渲染器”业务逻辑权威全在服务端。最后给个小技巧如果你不想让用户看到任何编辑痕迹可以把只读区域的鼠标样式改成not-allowed同时给Univer容器加一层监听在鼠标悬停到受保护单元格顶部时显示一个只读提示气泡。这个小改动很便宜但体验提升立竿见影。我的预算填报模块上线后业务方给出的评价就是“终于像个正经填报表了不像裸奔的Excel”。能做到这一点本质上不是靠某个神奇的功能而是把“保护开关、白名单、命令拦截、服务端校验”这四件事都补齐了。
返回列表