Univer 在线表格:实现指定单元格可填写、其他区域锁定的完整方案
最近接了一个要做“在线表格填单”的需求。逻辑听起来很简单:运营同事在系统后台自定义一张表格模板,前端把表格渲染出来,用户打开链接后只能填写其中指定的一部分单元格,比如“手机号”“参与人数”“备注”这些字段,其他单元格一律锁死,不能选中、不能修改。这个场景在表格组件里其实很典型,很多团队直接用 Excel 模板发下去再人工回收,效率很低。我调研了一圈后,最终选了开源表格组件 Univer 来做这套“Univer 在线表格”方案。如果你也在折腾 Univer 支持用户定义表格、指定单元格可填写、其他区域不可修改这类需求,这篇实操笔记应该能帮你少走不少弯路。
1. 为什么是 Univer:找一个能嵌入网页的“活表格”
1.1 这个需求真正要解决的不是“表格”而是“权限”
一开始我以为这无非是做一个 HTML 表单,放几个 input 就完事。但真和运营沟通完崩溃了:他们要的是一张和 Excel 长得一模一样的大表,包含列宽、行高、合并单元格、背景色、下拉选项、公式汇总,甚至还有条件格式。用纯 HTML 重建这些,每一次样式调整都是巨大工作量,根本不现实。
还有一条路是把 Excel 模板下载下去让用户填写,再上传回收。这套流程也能跑,但后果是版本满天飞、格式被改乱、手工汇总数据的时候想摔键盘。所以真正的核心需求是:保持 Excel 的交互和排版能力,同时做好“哪些单元格能填、哪些不能填”的权限控制。
Univer 支持用户定义表格,再把自定义好的表格放到网页上运行,这正好切中痛点。Univer 本身就是一个可在浏览器中运行的表格内核,渲染能力接近桌面表格软件,也保留了完整的单元格级控制能力。你可以用代码精确控制任意单元格的可写状态,再配数据校验和提交逻辑,整体体验和 Excel 收表完全不是一个量级。
1.2 对比过 Handsontable、Luckysheet 之后的取舍
在定 Univer 之前,我把市面上比较活跃的方案都过了一遍:
| 方案 | 开源/授权 | 单元格保护能力 | 项目维护与在线体验 |
|---|---|---|---|
| Univer | 开源,社区版可用 | 支持表格保护和单元格锁定,可精细控制 | 模块化设计,支持导入导出、公式、协同,更新活跃 |
| Handsontable | 商业授权,社区版功能受限 | 保护能力较弱,更多靠事件回调二次拦截 | 很成熟,但高级功能收费,商用要评估授权 |
| Luckysheet | 开源 | 有保护功能,但文档和更新节奏不太稳定 | 功能不少,但对前端工程化体验一般 |
我选 Univer 还有一个长远考虑:它本身瞄准的是文档、表格、幻灯片一体的方向,模块之间共用一套渲染内核。这次只做表格,但后续如果要加多表格页签、协同编辑、复杂公式计算,不用推倒重来。Univer 在线表格的场景今天能满足,往后扩展空间也够。
1.3 先建立最小认知:Workbook、Worksheet、Cell 和“保护”
Univer 的概念和 Excel 基本一致:
- Workbook:工作簿,对应一个完整的表格文件。
- Worksheet:工作表,工作簿里的一个页签。
- Cell:单元格,最小的数据单元。
- Range:连续单元格区域,比如 “A1:C10”。
- 保护:工作表级别的开关,开启后再结合单元格的 locked 属性来决定谁能编辑。
理解这套模型很重要。Univer 里“锁定单元格”这个行为,本质上是两层配合:第一层是给整张工作表开启保护,第二层是把允许编辑的单元格区域设置为 unlocked。这两层缺一不可,顺序也要处理对。后面我会展开讲。
2. 快速接入 Univer:把一张自定义表格跑起来
2.1 环境准备与依赖安装
Univer 是标准的前端组件库,对框架不挑剔。我的项目用的是 Vite + React + TypeScript,你也可以用 Vue 或者原生 TS 接入,核心流程差不多。
安装核心依赖的命令大致如下:
npm install @univerjs/core npm install @univerjs/sheets npm install @univerjs/ui如果你只是做原型验证,Univer 也提供了更高层的预设包,一次性把渲染、交互、公式等常用模块打包好。我建议小版本一定要锁住,因为 Univer 的 API 还在快速演进,不同版本之间的方法名和参数结构可能差得很多。装完先看一眼package.json里的版本号,再对照官方文档或类型声明写代码,能少踩很多坑。
2.2 初始化一个在线表格实例
最简单的初始化逻辑是:创建 Univer 实例并注册表格相关插件。以官方文档里的预设方式为例,代码看着大概是这样的:
import { createSheets } from '@univerjs/presets'; const univer = createSheets({ container: 'app', layout: { width: '100%', height: 600, }, });这段代码会往id="app"的 DOM 容器里渲染出一个完整的在线表格界面。如果你希望更精细地控制插件,可以手动注册核心插件:
import { Univer } from '@univerjs/core'; import { UniverSheetsPlugin } from '@univerjs/sheets'; const univer = new Univer(); univer.registerPlugin(UniverSheetsPlugin);实际项目里我更喜欢手动注册,因为可以只加载需要的模块,减少首屏体积。做“填表”场景时,很多用户不需要完整工具栏,只展示固定区域即可。
2.3 加载运营自定义的模板表格
自定义模板通常有两种进入方式:一种是直接把.xlsx文件导入进来,Univer 支持 Excel 文件格式的读写;另一种是通过 JSON 结构从后端拉取模板数据。我建议二选一,或者两者结合。
用 JSON 的好处是模板里的“锁定区域配置”可以一起管理,后端返回一段数据就能同时决定表格长什么样、哪些格子能填。简化后的模板结构类似:
{ "name": "项目周报模板", "sheetOrder": ["周报"], "sheets": { "周报": { "rowCount": 24, "columnCount": 8, "cellData": { "A1": { "v": "项目周报" }, "A3": { "v": "负责人:" }, "B3": { "v": "" }, "A5": { "v": "完成情况:" }, "B5": { "v": "" } }, "mergeData": [ { "startRow": 0, "endRow": 0, "startColumn": 0, "endColumn": 7 } ] } } }然后通过 Univer 的创建接口把这个模板装进去:
const workbook = univer.createUnit( 'WORKBOOK', template );注意,这段逻辑是简化后的方向性示例,具体字段名要以你当前版本的 TypeScript 类型为准。重要的是思路:模板结构来自后端,前端负责渲染和权限控制,不要把表格结构硬编码在前端。
3. 核心:让“指定单元格可填写,其他全部锁死”
3.1 先理解 Univer 的“锁定”底层逻辑
在 Univer 里,单元格的locked属性是样式的一部分,默认情况下所有单元格都是“锁定”状态。但锁定了并不意味着不能编辑,因为工作表还没开启保护。打个比方:每个格子都装着一把锁,但门没关,锁不锁都没意义。
所以要做“其他单元格不能修改”,必须同时做两件事:
- 给工作表开启保护。
- 把允许用户填写的区域,显式设置为“不锁定”。
只做第 1 步,会导致所有格子都不能填,连允许的区域也被锁死。只做第 2 步,不开启保护,用户仍然能到处乱改。这个顺序和组合,几乎是我被咨询最多的一个问题。
3.2 用 API 配置“保护工作表 + 解锁区域”
我在项目里的做法是封装一个函数,输入工作表实例和允许编辑的区域列表,统一完成配置。伪代码如下:
function enableEditRanges(sheet, editableRanges: string[]) { // 第 1 步:保护整个工作表 sheet.protect({ isProtected: true, options: { selectLockedCells: false, selectUnlockedCells: true, formatCells: false, formatRows: false, formatColumns: false, insertRows: false, insertColumns: false, deleteRows: false, deleteColumns: false, sort: false, filter: false, }, }); // 第 2 步:把允许填写的区域设置为不锁定 editableRanges.forEach((rangeAddress) => { const range = sheet.getRange(rangeAddress); range.setStyle({ protection: { locked: false, }, }); }); }selectLockedCells: false这个配置很关键。做填表场景时,用户选中一个锁定单元格,不应该出现任何可编辑光标,最好连选中框都不显示,这样体验更接近表单。selectUnlockedCells: true则保证用户能正常点击允许填写的区域。
如果允许填写的区域有很多块,比如“B3:B10”“D3:D10”“F3:F10”,直接传入一个数组即可。解锁之前最好先确认这些单元格没有被合并区域覆盖,否则会有边界问题,后面再细说。
3.3 给编辑动作再加一道保险:事件拦截
工作表保护能拦住绝大多数正常操作,但“粘贴”“拖拽”“双击填充”这类动作在部分版本里可能会绕过保护。为了万无一失,我会在编辑类命令执行前加一道校验。思路是:任何单元格内容变更,先判断目标坐标是否在允许列表里,不在就拒绝。
下面这段是概念性示例,具体命令名请以你的 Univer 版本为准:
univer.getCommandService().onCommandExecuted((command) => { if (command.id === 'sheet.mutation.editCell') { const { worksheetId, row, column } = command.params; if (!isAllowedEditRange(worksheetId, row, column)) { // 撤销或阻止这次编辑 command.stopPropagation?.(); } } });我用这个方式拦截过很多异常路径。用户从小键盘直接输入、Ctrl+V 粘贴、拖动右下角填充,这些操作的底层命令不同,但只要统一在“单元格变更”这个层面做白名单校验,就能保证“用户只能填写允许的区域”这个规则从上到下贯穿。
3.4 用户实际填写的交互流程
在 Univer 里做完权限控制后,我习惯把允许填写的单元格加一层浅黄色底纹,并在旁边写一段提示文字。用户打开链接后看到的效果是:
- 标题、说明、公式区域全部无法选中。
- 允许填写的格子有明显背景色,点击后直接输入。
- 部分格子配置了数据校验,比如只能选“是/否”。
- 填完点击“提交”按钮,前端收集数据,发送给后端保存。
这套交互比传统 Excel 收表清晰很多。Univer 支持用户定义表格,最终在浏览器里给出的是一个受限的、可控的表单。对填表用户来说,他们根本不需要理解 Excel,只需要点格子、填内容、点提交。
4. 实操中踩过的坑:局部只读表格的排查清单
4.1 最常见:设置了保护为什么还能修改
我在测试时最频繁踩到的坑是:工作表保护看起来开了,单元格锁属性也设置了,但用户还是能编辑。排查下来,大概率是下面几个原因:
| 现象 | 可能原因 | 解决办法 |
|---|---|---|
| 保护开启后全部不可编辑 | 没有给允许区域设置locked: false | 检查允许列表的单元格样式 |
| 允许的区域仍然不可编辑 | selectUnlockedCells被设置成 false | 设置为 true |
| 同一次操作中锁区和可编辑区同时生效 | 设置解锁样式时覆盖了原有样式 | 用 mergeStyle 方式保留原样式 |
| 模板加载后才做保护,但 UI 没刷新 | 表格渲染和权限设置顺序不对 | 在模板加载完成后再调用保护函数 |
有一点要特别提醒:setStyle在某些版本里是整体替换单元格样式。如果你同时设置了背景色、边框和protection,不小心就会把背景色覆盖掉。稳妥做法是先读取原样式,再把protection合并进去。
4.2 合并单元格、粘贴和拖拽填充分分钟绕过权限
合并单元格是重灾区。Univer 里设置区域解锁时,如果只对合并区域的左上角单元格设置locked: false,用户点击合并区域时可能还是无法编辑,或者状态不一致。解决方法是:把合并区域覆盖到的所有单元格都批量设置成locked: false,不要只写一个范围地址。
粘贴是第二个坑。用户从外部复制一整片数据到表格里,如果粘贴目标部分在锁定区、部分在可编辑区,表现会很奇怪。我建议把粘贴动作也放进事件校验:先算出粘贴的目标范围,再和目标区域的权限做交集,只允许粘贴到可编辑范围。如果粘贴范围超出了允许列表,直接弹提示并回滚。
拖拽填充和双击填充同理。比如用户双击某个可编辑单元格右下角,公式或者内容可能会自动填到下方锁定区域。这个动作也需要在命令校验里拦截。总之,不要只相信工作表保护,一定要在前端命令层做二次校验。
4.3 性能与体验优化:几十张表、几千行的真实场景
如果你的模板只有几十行,那性能都不是问题。但我的项目里有一张数据收集表,行数超过五千,还带公式。这个时候要关注几个点:
- 不要在页面一开始就把所有协同能力打开,单纯填表场景用单机模式够了。
- 模板数据尽量用 JSON 传到前端,不要在 Univer 里重复做复杂的 Excel 解析。
- 可编辑区域列表不要用后端字符串拆分拼范围,直接传结构化数组,减少前端转换开销。
- 给允许编辑的区域加上提示文字时,尽量用单元格批注或额外说明列,不要影响公式计算。
从用户体验来说,在线表格填单其实不应该给人“表格软件”的感觉,而应该给人“智能表单”的感觉。所以我刻意隐藏了大多数工具栏入口,只保留提交按钮和错误提示。Univer 在线表格的灵活性就在于,你可以把不需要的能力收起来,只露出业务需要的部分。
5. 落地到线上的关键配置:身份、模板、数据与部署
5.1 用户身份与模板版本联动
做真实项目时,“谁能改表格结构”和“谁能填哪些单元格”往往是两拨人。运营管理员进后台时,Univer 实例应该是可编辑的,能看到完整工具栏;普通用户进来时,实例应该只读,并且只能填指定区域。
我的做法是:登录后,后端返回当前用户的角色和模板 ID,前端根据角色渲染不同配置。管理员模板和用户模板用同一份 JSON 数据源,但权限配置不同。普通用户拿到的模板里,允许编辑区域就是动态接口给的那几个范围。运营调一下锁定范围,前端重新拉配置就能生效,不需要发版本。
5.2 数据提交与后端校验
用户填写完点击“提交”,前端需要通过 Univer 的读值接口,把允许编辑区域的数据提取出来。不要在前端拼好 Excel 再传后端,我建议直接传结构化数据,类似:
const result = { templateId: 'weekly-report', values: { B3: '张三', B5: '已完成', F3: '2025-06-01', }, };后端保存前一定要再校验一次:这些 key 是否都在模板允许编辑的范围内,内容格式是否满足校验规则。因为前端代码可以被改,锁定规则只起到引导作用,服务端才是最后一道闸门。如果你有协同编辑或者多人同时提交的需求,还要考虑版本冲突,至少保证同一张模板下所有人提交的数据不会互相覆盖。
5.3 部署时注意的打包体积和销毁问题
Univer 功能很全,因此打包体积不算小。生产环境里我会把 Univer 相关模块单独拆成一个异步 chunk,用户打开填表页时才加载。这样首屏主页面不会因为表格组件拖慢很久。
另外要注意 Univer 实例的生命周期。单页应用里路由切换后,如果旧实例不销毁,会残留监听器和内存占用。我在项目里会封装一个组件,在useEffect的清理函数里主动调用实例的释放方法,避免来回切换页面后内存只增不减。
如果只是做“用户填写提交”这种低频操作,建议每份提交独立初始化一张 Univer 表格,用完即毁。这样既避免数据串场,也让权限控制更清晰:每个人拿到的都是自己的私有实例。
最后分享一个小经验。我在这类项目里最看重的不是“哪个组件更像 Excel”,而是“锁区配置能不能独立运营”。把模板结构、允许编辑范围、数据校验规则都放到后端配置,前端只负责按配置渲染和收集数据,这套思路在 Univer 上落地后,运营改表真的不用再找开发改代码。还有一个小技巧:开发阶段先把官方示例里的“工作表保护”完整跑一遍,再叠加自己的样式和模板,很多坑都不会踩到。