☰
Univer在线表格单元格权限控制:只允许用户填写指定白名单区域
2026/11/3 5:43:41 网站建设 项目流程

第一回用Univer,纯属意外。我原本只是想把一张Excel报名表搬到网页上,让合作方自行填写,省得每天在微信群里收文件。试了好几款在线文档方案,要么太重,改起来像换了个系统,要么免费版限制太多。后来有同事提到Univer,说是个开源在线表格框架,能自渲染表格,也能嵌进自己的前端工程。我兴冲冲地把表格嵌进去,才意识到真正的麻烦才刚刚开始——表格是能显示了,但用户能改的东西实在太多了:表头、单位、说明、公式,任何一格都点进去敲键盘。这哪是收集信息,这是给用户发了个简化版Excel。

于是我开始琢磨,怎么在Univer上做一层“单元格可编辑权限”:让模板管理者自由定义哪些区域用户可以填,其他区域一律只读。折腾了几天,踩了几个不轻不重的坑,最终思路和代码都稳定下来了。如果你也想基于Univer做“用户只能填写指定单元格”的在线表格模板,这篇文章应该能帮你省下不少弯路。

1. 需求在哪儿:一张收集表为什么不能直接甩给用户

1.1 表格发出去之后,就完全失控了

很多人不理解,为什么在线表格非要搞“可编辑权限”。在传统工作流里,大家习惯的做法是:我先做一张Excel,通过邮件或微信发给别人,对方填完再发回来。这么做最大问题是,你根本管不住文件离开你手之后的任何变化。

随便举几个真实场景:

  • 活动报名表:用户把“部门”一栏的表头改成“所属中心”,汇总时根本对不上。
  • 仓库领料单:同事不小心删掉了一整列,拉到第二天下班才有人发现。
  • 数据录入表:某个字段自带公式,用户直接把公式覆盖成纯数字,后续统计直接裂开。

所以把表格放进网页,本质上是把“文件”变成一个“程序”。程序就一定要有输入规则:哪些数据允许进来,哪些动作不该发生,必须在一开始就定义清楚。

1.2 “不能改”并不等于“做成图片”

有人提出过偷懒方案:直接把表格渲染成图片,用户当然改不了,但那也填不了数据。于是又有人想,把表格拆成“展示表头和说明”加“旁边摆一堆输入框”,这就丧失了一个表格式交互的好处:数据的行列对应关系、批量录入、上下扫视、复制粘贴,全都没了。

实际上我们要的是一种折中:表格的绝大部分单元格保持只读,但指定的一部分单元格允许输入。用户仍然是在一个真实的表格里操作,只是操作范围被限制住了。只有Univer这类带完整编辑内核的开源表格框架,才有可能做到这种“半开放”状态。

1.3 需求最终收敛成两个模式

我在项目里把整个需求拆成了两个状态:

  • 模板编辑态:管理员打开表格,可以随便改单元格、加公式、改样式、合并区域,定义好哪些地方允许别人填。
  • 填写态:普通用户打开表格,只能往白名单区域里填内容,其余一律不可动。

两个状态切换的本质,就是对Univer发出的指令做一次动态校验。管理员这边放行所有命令,用户那边只放行白名单区域的命令。听起来简单,真正做起来还是有一些细节要考虑的。

2. Univer给了我们什么,又留了什么坑

2.1 Univer的核心优势:命令系统与协同引擎

Univer不是那种只能展示表格的组件库。它有几个底层特性,对做权限控制非常关键。

第一,所有操作都会变成命令。用户在界面上点选、输入、删除、改样式,Univer会把每次操作包装成一个Command对象,分发到命令系统里执行。也就是说,所有“用户想干什么”这件事,在真正修改数据之前,都会经过一个统一入口。这对做权限控制来说是天然的拦截点。

第二,数据变更是通过Mutation完成的。Mutation是真正写入数据的小颗粒动作,同时它也是协同端同步的最小单元。简单理解,命令是“我要改这里”,Mutation是“改这里这件事已经发生,可以同步给别人了”。两者层次不同。

第三,Univer本身就是一个可扩展框架。官方保留了插件注册机制,开发者可以把自己的逻辑嵌入到命令执行、渲染、交互等各个环节。它不想替业务方做判断,而是希望业务方在它的内核之上长出自己的规则。

2.2 官方没内置的细粒度锁定能力

到这里就该说清楚了:Univer目前没有开箱提供一个“单元格锁定/解除锁定”的配置项。新版竞选一些权限能力也有,但还没有到Excel那种“保护工作表并锁定特定单元格”的完整程度,也不会自动弹窗提示“该单元格受保护”。

这意味着我们不能靠点一个按钮解决问题,必须在框架上自己实现。好就好在,Univer的命令机制为这件事留了门:只要在正确的层级去拦截,就能做到“非法操作根本不会发生”,比Excel的保护机制还干脆。

但是这里有一个很关键的坑:拦截点如果选错了,会出现很诡异的问题。一开始我拦截的是Mutation层,也就是在数据已经准备写入的那一刻去阻止,结果用户第一次输入合法值时,撤销栈里记录了一条“已阻止”的空状态,第二次操作就报错,说撤销失败。后面我们把权限校验上移到Command层,问题才消失。

3. 核心思路:模板编辑态 + 填写白名单

3.1 先定义“哪些不能动”,比定义“哪些能填”更省事

权限方案有很多种写法。你可以做一套完整的行级、列级、单元格级ACL,也可以像我们最终做的那样:定义一个白名单区域。实践中我发现,白名单比黑名单好用得多。原因很简单,一张真实业务表格里,通常能填的字段就那么几个:姓名、电话、备注、数量,其他全是表头、说明、统计行,数量上不可编辑的区域远大于可编辑区域。用白名单描述“允许输入区”,规则少且直观。

数据结构可以设计成下面这样:

interface EditableRegion { sheetId: string; name?: string; ranges: Array<{ startRow: number; // 从0开始 startColumn: number; endRow: number; endColumn: number; }>; } // 例如:sheet-01 的 B2 到 E10 允许填写 const editableRegions: EditableRegion[] = [ { sheetId: 'sheet-01', name: '报名信息区', ranges: [ { startRow: 1, startColumn: 1, endRow: 10, endColumn: 4 } ] } ];

这个JSON既是前端拦截的依据,也是后端校验的依据,一份数据两处使用。管理员在编辑态可以重新调整白名单区域,保存后用户端重新加载即可。

3.2 操作分类:不是只有“改值”需要拦截

很多人以为,我只要拦下“修改单元格内容”的命令,权限控制就完了。真正做下去才发现,用户在表格里能干的事远比想象的多。我整理了一份分类表,把填写态下需要拦截的操作列全了:

用户行为涉及命令拦截策略
输入文字、数字SetRangeValuesCommand目标范围必须完全落在白名单内
粘贴内容PasteCommand / SetRangeValuesCommand粘贴解析后的目标范围超出白名单则阻断
拖拽填充DragFillCommand涉及白名单外区域直接阻断
修改样式、字体、颜色SetStyleCommand / SetRangeStyleCommand填写态一律禁止
清空内容ClearRangeContentCommand目标范围按白名单校验
合并单元格MergeRangeCommand涉及锁定区则禁止
插入行、删除行InsertRowCommand / DeleteColumnCommand影响白名单区域所在结构则禁止
移动行列MoveRangeCommand指向锁定区域则禁止

这个表未必100%覆盖所有Univer版本里的命令名,但它代表一个原则:权限控制要覆盖“任何可能改变表格数据或结构”的命令。只盯着改值命令,等于给用户留了一扇后门。

3.3 高亮可编辑区,给用户一个明确信号

权限控制不仅是技术策略,也是交互设计。用户不会天然知道哪些格子能填,你必须在视觉上给提示。

我的做法是,在进入填写态时,用Univer的单元格样式接口给白名单区域铺一层浅黄色背景,同时把白名单外的区域全部置为灰色。再在工作区顶部用一行引导文字,比如“仅黄色区域可填写,未锁定区域可以点击查看”。实测下来,不明所以的咨询少了很多。

对管理员,编辑态下区域背景是默认白色,方便调整内容;对用户,黄色区域一眼就看明白,体验很直观。

当然,这个视觉区分要在“真正写入数据”之前做好。如果等用户点击格子才弹出提示,效果就差很多。

4. 命令拦截落地:权限门前移

4.1 初始化最小Univer项目

先按Univer官方方式创建一个最小项目。不同版本API略不同,我贴一下目前比较常用的写法,具体版本信息要以你安装的包文档为准。

import { Univer, UniverInstanceType } from '@univerjs/core'; import { UniverSheetsPlugin } from '@univerjs/sheets'; import { UniverSheetsUIPlugin } from '@univerjs/sheets-ui'; import { UniverUIPlugin } from '@univerjs/ui'; const univer = new Univer(); univer.registerPlugin(UniverUIPlugin, { container: document.getElementById('app'), locale: 'zhCN', layout: { toolbar: ['undo', 'redo', 'font', 'font-size', 'bold', 'italic'] } }); univer.registerPlugin(UniverSheetsPlugin); univer.registerPlugin(UniverSheetsUIPlugin); // 创建Workbook const workbook = univer.createUnit(UniverInstanceType.SHEET, { id: 'workbook-001', name: '报名表模板', sheets: [ { id: 'sheet-01', name: '报名表', rowCount: 20, columnCount: 8, cellData: { 0: { 0: { v: '姓名' }, 1: { v: '手机号' }, 2: { v: '备注' } } } } ] });

初始化完成后,Univer实例里就能拿到命令服务,这是后续所有拦截逻辑的基础。

4.2 封装一个范围检查工具

范围判断是权限核心,必须稳定可靠。我封装了一个函数,判断用户选中的矩形区域是否完全落在某个白名单区域内。

function isRectInEditableRanges( startRow: number, startColumn: number, endRow: number, endColumn: number, editableRegions: EditableRegion[], sheetId: string ): boolean { const regions = editableRegions.filter((region) => region.sheetId === sheetId); if (regions.length === 0) return false; // 遍历所有可编辑区域,只要有一个区域完全包含目标矩形,就算允许 return regions.some((region) => region.ranges.some((range) => { const isRowOk = startRow >= range.startRow && endRow <= range.endRow; const isColumnOk = startColumn >= range.startColumn && endColumn <= range.endColumn; return isRowOk && isColumnOk; }) ); }

这里建议用“完全包含”而不是“有交集”。举个例子:如果白名单是B2:D5,用户试图从C4拉到F6,就有两列落在白名单外。如果只用“有交集”判断,会把C4:D5这部分放行,但F6明显不该改。对收集表来说,宁可严格一点,也不能让用户在锁定区写入任何内容。

4.3 在Command层挂上权限校验

Univer命令服务提供监听器。我在项目里用的钩子大致是这样:

import { CommandType } from '@univerjs/core'; const commandService = univer.getCommandService(); commandService.beforeCommandExecuted((command) => { // 填写态才校验,编辑态全部放行 if (currentMode !== 'fill') { return true; } // 拦截修改单元格值的命令 if (command.type === CommandType.COMMAND && command.id === 'sheets.command.set-range-values') { const params = command.params as any; const ranges = extractRangesFromParams(params); for (const range of ranges) { const allowed = isRectInEditableRanges( range.startRow, range.startColumn, range.endRow, range.endColumn, editableRegions, currentSheetId ); if (!allowed) { showToast('该区域受保护,无法编辑'); return false; // 阻止命令执行 } } } // 样式类命令、结构类命令,填写态直接禁掉 const restrictedCommands = [ 'sheets.command.set-style', 'sheets.command.insert-row', 'sheets.command.insert-column', 'sheets.command.delete-row', 'sheets.command.delete-column', 'sheets.command.merge-range', 'sheets.command.clear-range-content' ]; if (restrictedCommands.includes(command.id)) { showToast('当前模式下不允许该操作'); return false; } return true; });

这段代码有两点值得强调。

第一,不要动Mutation层。Mutation是数据落地和协同同步的地方,如果在那里拦截,容易破坏撤销栈和协同状态,我已经踩过这个坑了。

第二,返回false代表命令不执行。Univer命令系统执行命令时会判断前置结果,如果前置返回false,后续函数不会傻傻地执行。这个机制能保证非法操作真正被拦下来,用户不会看到数据被改掉一半。

4.4 解析目标范围:必须拿真实的range

命令参数里的range解析是另一个容易出错的地方。不同命令的参数格式不同,SetRangeValuesCommand里可能带多个range,也可能只有一个range,还有可能是按单元格逐个key-value的格式。我建议做一层适配器,把命令参数统一转成range数组。

伪代码:

function extractRangesFromParams(params: any): Array<Range> { // 如果 params.ranges 存在就直接用 if (Array.isArray(params.ranges)) return params.ranges; // 如果 params.cellValue 存在,就按对象的 row / col 结构还原 if (params.cellValue) { const ranges: Range[] = []; const sheetId = params.sheetId || currentSheetId; // 遍历 cellValue 的所有 key,parse 行号和列号 const rows = Object.keys(params.cellValue).map((row) => parseInt(row)); const cols = Object.keys(params.cellValue[rows[0]] || {}).map((col) => parseInt(col)); const minRow = Math.min(...rows); const maxRow = Math.max(...rows); const minCol = Math.min(...cols); const maxCol = Math.max(...cols); ranges.push({ sheetId, startRow: minRow, startColumn: minCol, endRow: maxRow, endColumn: maxCol }); return ranges; } return []; }

如果不做适配,像点击一个单元格输入和批量粘贴一大片区域这两种方式,可能走不同的参数结构,漏掉一种会导致锁定区域被绕过去。

5. 一个课程报名案例跑通的完整链路

5.1 定义模板:从空白工作表到完整报名表

我用一个具体案例说明整个流程。假设要做一个内部课程报名表,需要学员填写姓名、部门、手机号、期望上课时间,除此之外表头、项目说明、开课地点等都由管理员掌控。

管理员在界面上进入“模板编辑态”:

  1. 创建新表格,写好标题和表头。
  2. 在B2:E10区域预留可填写区,并提前输入示例文案。
  3. 用配置面板把B2:E10标记为“允许填写”,保存到后端。
  4. 数据库中存两份数据:一份是模板内容(cellData),一份是权限配置(editableRegions)。

用户端的加载逻辑则变成:

async function loadFillMode(workbookId: string) { const { cellData, editableRegions } = await fetchTemplate(workbookId); // 先用模板数据渲染Univer表格 renderSheet(workbookId, cellData); // 切换到填写态 switchToFillMode(editableRegions); }

5.2 填写态用户的实际体验

用户打开页面后看到的是:

  • 表格整体是灰底,等于在暗示“这里不能乱动”。
  • 只有B2:E10是黄底,用户点进去可以正常输入。
  • 用户想改表头,点击后完全没反应,或者弹一个“该区域受保护”的提示。
  • 用户粘贴一大片数据,如果粘贴范围超出了黄色区域,整次粘贴被拦截,不会出现“粘了一半数据”的脏状态。

这里最需要细心的地方是:即使只允许填一个2行3列的区域,用户也可能通过拖拽填充把内容覆盖到旁边区域,必须一并拦下。我在第3节把DragFillCommand列入禁止名单就是这个原因。

5.3 提交数据:前端只做引导,后端才是最终防线

前端命令拦截解决的是“误操作”,解决不了“恶意提交”。一个懂行的用户完全可以用DevTools绕过前端逻辑,直接构造请求把你的数据写进去。所以后端必须做同样的权限校验。

后端伪代码示例:

// Node.js / Express 处理提交 app.post('/api/workbook/:id/cells', async (req, res) => { const workbookId = req.params.id; const { sheetId, ranges, values } = req.body; const template = await getTemplate(workbookId); const editableRegions = template.editableRegions; for (const range of ranges) { if (!isRectInEditableRanges(range, editableRegions, sheetId)) { return res.status(403).json({ error: '该区域受保护' }); } } // 校验通过后才真正写入数据 await applyCellValues(workbookId, sheetId, ranges, values); res.json({ ok: true }); });

这个“前端拦截 + 后端校验”的双层结构,才是完整方案。前端保证体验,后端保证安全,两边共用同一份白名单规则。

5.4 管理员的汇总和查看

收集完成后,管理员切回“模板编辑态”,可以直接在同一个Univer实例里查看所有数据。如果数据量不大,直接读库里的最新cellData渲染出来即可;如果数据量很大,建议在管理端做一个只读表格视图或分页加载,而不要一次性把所有单元格的数据全塞给前端。

6. 协同场景和撤销栈:最容易翻车的两处

6.1 坑一:在Mutation层拦截导致撤销栈错乱

这个坑我在前面提过,现在详细说说。

Univer的撤销栈记录的是Mutation。也就是“这次数据变更是否有效,能否撤销一次”。我在第一个版本里,因为不喜欢在Command层解析参数,就图方便直接拦Mutation,在Mutation执行前检查范围,非法就返回false。

打开第一个测试案例时发现,用户第一次输入合法值时,一切正常。但是接着在另一个锁定区域输入内容,这时弹窗提示“受保护”,然后用户又回到可编辑区域输入,撤销按钮就坏了。点撤销没有反应,再点一次,整个页面还卡了一下。

原因推测是,被拦截的Mutation已经进入了撤销记录,但它实际上没有修改数据,撤销时找不到对应的反向操作,状态就错了。后来把校验逻辑整体上移到Command层,Mutation层保持纯净,撤销栈就恢复正常了。

6.2 坑二:复制粘贴会带进来不只一个命令

第二个坑更隐蔽。用户从外部Excel复制了一段带颜色、带边框的内容,粘贴到表格里。很多框架处理粘贴不是单纯“写入值”,而是先写入值,再同步设置样式、合并单元格等。我在权限白名单里只拦了SetRangeValuesCommand,结果用户把外部内容贴进来时,值被成功拦截了,但样式命令没拦住,锁定区域外的格子被贴上了红色背景和边框,看起来非常诡异。

最终处理方式是:禁止填写态用户的粘贴类命令,连PasteCommand一起拦。对于可编辑区域的粘贴,可以先清一下目标区域样式,再写入纯文本值,避免外面乱七八糟的格式带进来。

6.3 坑三:协同编辑时不能拦截远端Mutation

如果项目开了协同编辑,还要注意一个问题:别人(比如管理员)在另一端编辑了某个格子,这条变更会以远端Mutation形式同步到当前端。如果在当前端对远端Mutation也做权限拦截,负责人把数据改好之后,别的用户刷新页面可能发现数据没变,或者数据被本地权限规则回滚了。

正确的做法是:权限拦截只针对本地用户操作触发的命令,远端同步的Mutation要放行。本地命令拦截相当于“关上门不让用户走过去”,远端Mutation相当于“别人从另一扇门合法进来了,不能拦”。判断本地还是远端,通常看命令来源标记,Univer的命令里会带来源信息,做一下区分即可。

还有一个附加建议:开协同后,服务端校验要更严格。同一张表格同时被多人填写时,有可能出现交叉写入。白名单规则在后端校验,至少能保证“非法区域的值”写不进去。

7. 一些实操心得和最终建议

在上面这套方案跑了大半年之后,我最大的感受是:Univer做这种“半开放表格”是完全能扛住的,关键是权限控制的思路要对。

如果你现在也准备用Univer做收集、登记、排期之类的场景,我建议不要一上来就写权限系统。先把最简单的表格嵌入和基础编辑流程跑通,让业务方在真实数据上确认字段和布局,然后再把“白名单 + 命令拦截”这套机制加进去。权限模型如果一开始设计得太复杂,比如行列级ACL、角色矩阵、分组权限全上,后期维护成本会非常高,业务方自己也会晕。

最终我们这个项目保存下来的,就是一个很朴素的JSON白名单,加一个命令层拦截器,加一套后端校验接口。够用,稳定,也容易扩展。往后如果还要支持多人分角色填写不同区域,无非是把editableRegions从单一集合变成按角色分组的集合,然后在校验器里多传一个当前角色参数,整体思路完全不用推翻。

如果你在部署过程中也遇到了别的奇奇怪怪的命令穿透问题,欢迎用同样的思路排查:先看操作从哪个命令进入,再把这个命令加进拦截名单。逐条堵漏,很快就能把“非法操作”从路径上彻底赶走。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询