☰
Univer实现指定单元格可编辑:在线表格锁定与数据填报实践
2026/11/2 14:05:26 网站建设 项目流程

这两年只要聊开源在线表格,绕不开 Univer 这个名字。它做的事情和 Google Sheets 类似,但你可以完全自己部署、自己二开,把它嵌到自己的产品里。我最近在做一套内部数据填报系统,核心需求刚好就是网上常被搜到的那句话:univer 支持用户定义表格,用户去填写某些单元格,其他单元格用户无法修改。这篇我就把从选型到落地的完整过程写下来,包括为什么选 Univer、单元格锁定背后的原理、具体怎么实现只让用户填写指定格子,以及我在真实项目里踩过的坑。对正在做在线填报表单、或者考虑自建轻量级表格产品的朋友,应该有直接参考价值。

1. 为什么我选了 Univer:一个在线填报需求引发的选型复盘

1.1 先搞清楚需求:用户要填的到底是一张表,还是一个表单

在做技术选型之前,我先把业务需求捋了一遍。表面上这是一张表格,实际上它是一个带固定模板的表单:HR 收集员工信息、运营收集渠道数据、供应商在线报价格,都是同一类场景。用户不需要学会 Excel,不需要理解公式,他们只需要看到一张排版好的表格,然后把光标放到对应的空白格子里输入内容。真正难的不是把表格做出来,而是管住哪些格子可以动、哪些格子不能动。如果只拿一张普通的在线表格给人用,用户几乎必然会把表头改掉、把别人的内容删掉、把格式弄得一团糟,最后你根本没法汇总数据。

这个问题一描述出来,你会发现它和 Excel 的“保护工作表”能力完全对应。所以我一上来就按这条路子找方案:支持单元格锁定的在线表格,而不是一个简单的富文本编辑器式的表格控件。Univer 恰好在这个点上给了我足够的信心,它的存在,让我可以专注于业务层的“哪些区域开放”逻辑,而不用从零写选区交互和渲染引擎。

1.2 横向对比:主流开源表格方案里,谁 Lock 能力真正可用

在决定之前我把市面上能用的方案都过了一遍。Luckysheet 前几年很火,Github star 也不少,但维护基本停滞了,遇到浏览器升级或者需要深度定制时只能自己上,风险和成本都偏高。x-data-spreadsheet 很轻量,做静态展示还行,但你找不到像样的单元格锁定、保护工作表、协同编辑这类能力,离“生产可用”差得远。Handsontable 提供了锁定和权限相关的机制,不过它本质上更像一个可编辑的数据网格,而不是完整的电子表格,而且商业化授权是一道现实的门槛。SpreadJS 功能确实强,是商业产品,价格也透明地摆在那里,适合预算充足、不想折腾的团队,但对想要自主可控、灵活定制的团队来说不是首选。

方案是否开源单元格锁定/工作表保护活跃度适用感受
Luckysheet是弱停滞演示有余,生产风险大
x-data-spreadsheet是无停滞简单展示可以
Handsontable部分支持一般更像数据网格,授权要评估
SpreadJS否支持商业维护功能强,费用高
Univer是支持(Excel 同款语义)极高可二开,适合产品化

Univer 的优势在于它把 Excel 那套“单元格 locked 属性 + 工作表保护”的双层机制做出来了,而且项目活跃度非常高,背后有团队在持续迭代,社区里也能看到不少真实案例。对技术选型来说,一个持续演进的项目比一个表面上“什么都有”但没人维护的项目要安全得多。

1.3 选型的三个硬指标

我给自己定了三个硬指标。第一是能锁定,这是核心中的核心,必须支持单元格级的 locked 语义和工作表保护。第二是能嵌入现有前端工程,我团队是 Vue + TypeScript,最好是不绑定全家桶的库,能按需集成。第三是持续活跃,表格这种重前端业务,库一旦停更,浏览器升级几次以后就得返工,维护成本会失控。Univer 恰好三条都满足:它提供 npm 包,可以接 Vue/React 任意框架;它的核心渲染基于 Canvas,处理大数据量时比 DOM 方案稳得多;它的插件化架构让我可以在不破坏主流程的前提下做定制。

自研这条路我也认真想过,但很快就放弃了。表格的选区交互、行列冻结、公式引擎、单元格样式解析,每一个模块都是一年以上的投入。对大多数团队来说,用成熟开源项目做底座,把精力放在业务逻辑和用户体验上,才是最合理的路径。

2. 核心设计思路:指定单元格可编辑的底层逻辑

2.1 “锁定/保护”到底是怎么运作的

在 Excel 和 Univer 里,“这个格子能不能编辑”是两层判断叠加出来的。第一层看单元格样式里的 locked 属性,第二层看工作表是否处于保护状态。默认情况下所有单元格的 locked 都是 true,但只要你没开保护,这个属性就形同虚设,格子怎么点都能编辑。一旦工作表开了保护,locked=true 的格子就进入只读状态,locked=false 的格子仍然可以编辑。

看到这里你应该明白了一个关键点:想让用户只能编辑某几个单元格,你要做的是“把整张表锁起来,然后给目标区域单独开锁”,而不是“顺着把所有单元格一个个设置成只读”。前者只要一次保护命令和一条区域解锁命令,后者要遍历上万行,还容易漏。

我把这个机制类比成印刷好的纸质表格:出厂时每个格子上面都覆着一层膜,工作表保护就是“把整张表用膜盖住”,解锁某个区域就是“用刀片在对应位置划开”。用户只能往划开的位置写字,其他地方怎么用力都写不进去。这个类比帮助我向产品和 UI 同学解释需求,也让开发时的思路变得特别清晰。

2.2 三种实现方式对比

针对“管理员定义表格、用户填指定单元格”的需求,我梳理出三种实现方式,适用场景完全不同。

实现方式典型做法适用场景优点缺点
UI 手动配置管理员在表格编辑器里设置单元格锁定,然后开启工作表保护内部少量模板,运维人员自己维护零代码,直观难以批量操作,权限不易审计
静态模板代码锁定前端把可编辑区域坐标写死,初始化时批量解锁并保护模板长期固定、角色单一简单、可版本管理不灵活
动态权限后端返回可编辑区域列表,前端按接口配置解锁多角色、多模板、SaaS 场景可扩展,支持权限管理需要接口和权限设计

我的项目属于第三种,因为系统里同一种模板要给几十个用户使用,不同用户看到的可编辑区域还不一样。但即便你只做内部工具,我也建议把“解锁区域”设计成可配置的数据,而不是直接散落在代码里。这样以后加模板、加角色,都不需要动前端逻辑。

2.3 数据模型怎么设计

把模板、实例、权限三层分开,是我觉得整个方案里比较值得说的一点。模板层只负责表头、字段名、合并单元格这些样式结构;实例层是每个用户打开时看到的、填充了实际数据的表格;权限层记录哪些单元格对这个用户开放。这三层混在一起,后续维护会非常痛苦。

我最后用的配置结构大致是这样的:

{ "templateName": "客户信息登记表", "lockedAreas": ["A1:D2", "A3:A10"], // 这些区域锁定,只读 "editableAreas": ["B3:D10"] // 这些区域开放给用户填写 }

实际存储时我会把 editableAreas 拆成一行行的区间对象,比如{ startRow: 2, startColumn: 1, endRow: 9, endColumn: 3 },方便程序直接使用。这样做的好处是,后端权限引擎只需要判断“某个用户请求的模板 id 和角色能不能匹配”,前端只需要负责把对应的区间解锁,职责边界非常清楚。

3. 实操:在项目中跑通 Univer,并实现限定单元格可编辑

3.1 环境准备:依赖安装与最小启动

我项目里用的是 Vite + Vue3 + TypeScript,Univer 的官方推荐就是通过 npm 包管理依赖,然后按需注册插件。安装命令很简单:

npm install @univerjs/core @univerjs/sheets @univerjs/sheets-ui @univerjs/ui

包名会随版本迭代有些变化,这很正常,Univer 迭代速度很快,装完记得看一眼 node_modules 里的类型定义。初始化一个最小实例的代码大致如下:

import { Univer, UniverInstanceType } from '@univerjs/core'; import { UniverUI } from '@univerjs/ui'; import { UniverSheetsPlugin } from '@univerjs/sheets'; import { UniverSheetsUIPlugin } from '@univerjs/sheets-ui'; const univer = new Univer({ locale: 'zhCN', }); univer.registerPlugin(UniverUI, { container: 'app', // 页面上放一个 id 为 app 的 div }); univer.registerPlugin(UniverSheetsPlugin); univer.registerPlugin(UniverSheetsUIPlugin);

到这里,浏览器里就已经能看到一个可以交互的在线表格了。注意引入一下入口样式,否则界面会裸奔,CSS 路径每个版本略有不同,以官方文档为准。所谓“univer 在线”,本质就是这个流程:前端构建产物部署到任意静态服务器,用户打开浏览器即可使用,不需要安装任何桌面软件。

3.2 核心场景实现:让用户只在指定单元格填内容

接下来是重头戏。我创建一个最简单的表格,表头是“姓名”和“联系方式”,下面留两行让用户填写。然后我要做的只有两件事:解锁可编辑区域,保护工作表。

const workbook = univer.createUnit(UniverInstanceType.SHEET, { name: 'demo', sheets: { sheet1: { cellData: { '0': { '0': { v: '姓名' }, '1': { v: '联系方式' } }, '1': { '0': { v: '' }, '1': { v: '' } }, '2': { '0': { v: '' }, '1': { v: '' } } } } } }); const sheet = workbook.getActiveSheet(); // 1. 先把需要用户填写的区域解锁 // 这里希望 B1:C2 可填写,实际坐标从第1行第1列开始,共2行2列 const editableRange = sheet.getRange(1, 1, 2, 2); editableRange.setStyle({ locked: false }); // 2. 再保护整张工作表,locked 的格子全部禁止编辑 sheet.protect({ enableProtection: true, selectLockedCells: false, selectUnlockedCells: true, });

这段代码里有个细节值得展开说。我先设置锁定样式,后开保护,顺序我建议不要反过来。某些版本里 protect 命令执行时会读取当前状态并缓存,如果你先开保护再 setStyle 解锁,解锁结果可能被保护快照盖掉,出来还是只读。先把 unlocked 标好,再上保护,逻辑上最直观,也最不容易踩坑。

另外,代码中用 getRange 批量处理,而不是 for 循环一格格 setStyle,这是个性能习惯问题。一次性给 2 行 2 列解锁感觉不出区别,但当你面对 5000 行可编辑区域时,批量处理和单格处理之间的差距就是秒级和卡顿的区别。

3.3 把填好的数据提交给后端:别把样式一起带走

用户填完数据之后,按一个“提交”按钮,前端把数据发给后端。这一步看起来简单,但有个坑我必须提前说:读数据时不要无脑导出整个 sheet 快照,因为你可能连样式、锁定标记、甚至用户误操作留下的格式一起发出去了。我只读取目标区域的值:

// 读取 B1:C2 的值,返回二维数组 const values = sheet.getRange(1, 1, 2, 2).getValue();

拿到 values 以后,先在前端做一轮基础校验:必填项、手机号格式、数值范围。然后 POST 到自己的接口。整个提交动作完全可以做成一个“提交”按钮,放在表格工具栏旁边的自定义位置,用户完全感知不到底层是一套电子表格引擎。

有一点必须反复强调:前端校验只是体验优化,真正去重、校验和防篡改必须在后端做。因为任何一个懂 devtools 的人都可以修改 POST 的 payload,前端锁定的单元格只能防普通用户的误操作,防不了专业用户刻意传假数据。所以在设计接口时,后端要重新校验提交内容,并对只读区域的数据直接忽略或报错,而不是盲目信任前端给的单元格数据。

3.4 动态化:多角色、多模板的权限下发

如果你的系统里不同角色看到的可编辑区域不一样,就需要把前面写死的解锁区域改成接口返回。做法很简单,把权限配置放到后端,前端初始化表格后请求一次,然后批量解锁:

const config = await fetch('/api/sheet-config') .then(r => r.json()); const sheet = workbook.getActiveSheet(); // config.editableAreas 是后端根据当前用户角色算好的区域 config.editableAreas.forEach(({ startRow, startColumn, endRow, endColumn }) => { sheet.getRange(startRow, startColumn, endRow - startRow + 1, endColumn - startColumn + 1) .setStyle({ locked: false }); }); sheet.protect({ enableProtection: true, selectLockedCells: false, selectUnlockedCells: true, });

这种动态方案把模板和实例串起来了:管理端定义模板时保存固定区域,用户打开表格时后端根据“用户角色 + 模板 ID”算出可编辑区域,前端统一执行解锁和保护。后面的迭代方向就是给这套逻辑加一个配置页面,让非技术人员也可以自己建模板,本质上就是一个轻量级的数据填报平台。

4. 避坑实录:用 Univer 做填报表格踩过的 5 个坑

4.1 解锁了还是不能编辑?先检查保护开关和版本 API

这是最常见的现象:明明对区域做了 unlocked 标记,用户点进去还是只读。我排查下来的原因通常有两个。第一,只做了 setStyle,没有真正开启 protect,这种情况下所有单元格都能编辑,和你预期相反;第二,protect 和 setStyle 的调用顺序不对,或者版本 API 有变化,导致解锁区域没被最终生效。

Univer 的迭代速度确实快,不同版本的接口有时会从sheet.protect()改成commandService.executeCommand或者新的模块 API。遇到这类问题,不要瞎猜,直接去看本地 node_modules 里对应包的类型声明,或者看官方文档对应版本。把“版本 API 差异”这个变量单独拿出来排错,可以帮你省下大量时间。

4.2 复制粘贴把锁定状态也带走了

用户从锁定区域复制内容,再粘贴到可编辑区域时,Univer 默认会把样式也粘贴进来,其中就包含 locked 属性。如果粘贴过去的样式是 locked=true,那么你表面上标记好的可填写格子,会被用户的一次复制粘贴操作重新锁死。反过来也一样,从开放区复制到只读区,会把 locked=false 带过去,保护的口子就漏了。

这个问题的处理思路,我在实际项目里是双管齐下:第一,提交数据时只读单元格 value,不读样式,这样即使样式被污染,也不影响入库;第二,在“提交”按钮点击时,先校验一遍可编辑区域内的单元格是否都是 unlocked,如果发现被覆盖成锁定,就提示用户重新填写或者前端强制重置该区域样式。这个方法不算优雅,但简单有效,能把风险控制在可接受的范围内。

4.3 保护后菜单被禁用,产品和开发要有共识

一旦开启工作表保护,Univer 会按保护配置把插入行列、删除行列、调整列宽、格式化、排序等操作一并限制掉。对填报表单来说,这通常正是想要的。但产品同学有时候会提“列宽能不能拖一下”“这个表格能不能筛选”,这时候千万别为了一个筛选功能把整个保护关掉。正确做法是在保护配置里细粒度放开某个能力,同时仍然挡住单元格编辑。如果一刀切关保护,前面所有的锁定工作都会失效,数据被改坏了再后悔就来不及了。

我的经验是,在第一版实现前就和产品对齐一句话:锁定解决的是“谁改数据”的问题,不是“能不能看数据”的问题。筛选、排序、冻结这些是查看体验,应该尽量开放;编辑、删除、插入这些是写操作,必须收紧。把这个原则定下来,后续需求评审会顺畅很多。

4.4 大量区域解锁时的性能陷阱

如果你的模板里有大量可编辑行,比如几千行,不要用循环一格格去 setStyle。批量 Set Range 的性能和单格遍历差别非常大,具体差多少取决于浏览器,但用户体验上基本就是“秒开”和“卡死”的区别。我在项目里用 2 万行空数据测试过,初始化明显掉帧,后来把空行砍掉,只保留实际使用到的行数,问题立刻消失。

另外,Univer 是按需渲染的,但如果你在创建 sheet 的配置里把几十万行 cellData 都塞进去,内存依然会爆。我的习惯是至少要保证 sheet 的“数据范围”等于“视觉范围”,不要人为制造大范围空洞。等用户真的需要扩张再动态加行,这个思路放哪个表格引擎里都成立。

4.5 常见问题速查表

现象可能原因解决思路
所有单元格都不可编辑开保护但没解锁任何区域先对目标区域 setStyle({ locked: false })
解锁区域依然只读调用顺序/API版本问题检查 protect 参数,先解锁后保护
提交后数据带了格式直接导出了全量快照只读需要的 value,过滤样式
用户复制粘贴后格子状态乱了locked 属性被粘贴内容覆盖提交前校验只读区域,重置样式
页面初始化卡顿空行/空列范围太大精简 cellData,按需扩充行

5. 最后:一点个人体会

我在实际项目里的体会是:Univer 并不是一个开箱即用的“问卷表单工具”,它的强项是底层的表格能力和插件化架构,你需要自己把“锁定哪些区域、开放哪些区域、提交到哪”这三件事组织起来。但这个成本非常值,因为当你做完第一次,后面所有填报表单都成了一个配置项,新增模板只是配置数据,不再需要开发介入。

最后再分享一个小技巧:调试锁定逻辑时,可以先不开保护,把 unlock 的区域用背景色标出来,肉眼确认坐标没有写错,再加保护。这一招帮我省下了大量“为什么这块不能填”“为什么那块能填”的排查时间。Univer 这个方向我后续还想继续挖,比如接入协同评论、加入数据校验下拉、把填报表单做成可拖拽的区块,这些在它的插件机制下都有落地空间。

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

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

立即咨询