☰
Univer实战:模板表格指定单元格可编辑与工作表保护完整指南
2026/10/1 4:39:53 网站建设 项目流程

前阵子有个朋友找我,说他们业务系统里要做一张“只能填一部分格子的在线表格”——表头、指标、公式都是固定的,用户只需要在某几列录入数据,其余单元格死活不能动。我一听,这不就是典型的数据填报场景嘛。关键是他需求里明确点了一个名字:Univer。

Univer 是目前开源圈里相当能打的在线表格/文档内核,用 TypeScript 写的,渲染性能、插件机制、协同能力都在线,很多人拿它做定制化的在线 Excel 或数据采集系统。这篇文章就围绕 Univer 这个项目,聊聊怎么把它用好。重点是“让用户定义表格模板、指定单元格可填写、其余单元格锁定”这套能力怎么落地,包括原理拆解、完整实操步骤、以及我实际跑下来踩过的坑。

1. 从零认识 Univer:它凭什么成为在线表格的内核之选

1.1 先搞清楚 Univer 到底是什么

Univer 不是一个简单的“网页版 Excel”,它更像一个“办公套件内核”。项目本身支持表格、文档、幻灯片三类能力,其中表格(Univer Sheet)是现在最成熟、被用得最多的部分。它底层用 Canvas 做渲染,而不是像很多老项目那样直接操作 DOM,所以大数据量滚动、公式计算、单元格交互都比传统方案流畅得多。

核心优势我认为是三点:第一,开源可定制,不像 SpreadJS 那样有严格授权限制,也不像某些 SaaS 产品那样只能用它给你规定好的 UI;第二,插件化架构,从工具栏、菜单到单元格渲染器都可以按需替换,很适合嵌进自己的业务系统;第三,数据模型和 UI 分离,底层是一套类似文档快照的数据结构,上层 UI 只是它的“视图”之一。这套设计对做“模板表格 + 部分可编辑”这种需求特别友好,后面我会详细说。

很多人拿它和 Luckysheet 比较。Luckysheet 是“开源表格”的先行者,目前已经闭源转向了商业产品。Univer 的社区活跃度、类型系统、协同支持明显更好,而且在前端基于 TypeScript,对于要长期维护的工程来说,类型提示能帮你省掉大量低级错误。

1.2 数据模型与快照:为什么模板能力让人省心

Univer 对“表格”的理解是一份完整的工作簿快照(Snapshot)。快照里包含工作簿的名称、工作表列表、每个 sheet 的行数、列数、单元格数据、样式、行高列宽、合并单元格、数据校验规则等等。

这意味着什么?意味着整个表格模板就是一段可持久化的 JSON 数据。你可以把它存在数据库里,也可以放到对象存储上,用户打开页面时加载快照,Univer 按快照渲染出一张完整的表。模板不需要“现场用代码一条一条画格子”,本质上和 Excel 文件是一个思路,只是变成了 JSON 格式。这也给“用户定义表格”提供了很自然的实现路径:先让管理员在线编辑出模板,把快照存下来,再分发给普通用户去填写。

很多人第一次用会忽略一个问题:快照要版本化。模板一旦发布出去,后面一定会不断改版。如果不做版本管理,就会出现“用户正在填的表”和“管理员刚生成的模板”对不上的情况。

1.3 命令系统:所有操作都可控、可拦截

Univer 的另一个核心设计是 Undo/Redo 命令体系。单元格编辑、插入行列、设置样式、保护工作表,这些操作在内部都是一条条“命令”。命令执行前会经过事件总线,外部代码可以监听、拦截,甚至阻断。

这给我们做“只允许填写指定单元格”提供了极大的灵活性。有两种实现思路:

  • 数据模型层面:使用工作表保护,锁定整个表,再单独解锁允许编辑的范围。这是最可靠的做法,用户不管怎么操作都改不动锁定区域。
  • 交互拦截层面:监听编辑命令,判断选区是否在允许范围内,不合法就取消命令。这种做法像是“门禁卡”,能拦住大部分常规操作。

我个人的建议是,优先用“数据模型层面”的方案,交互拦截作为兜底。因为保护机制存在于数据结构里,更接近真实权限控制,不容易被“换个入口绕过”这类问题击穿。

2. 需求拆解:让单元格只读、指定单元格可写,本质在解决什么问题

2.1 典型场景:模板表格 + 指定填写区域

先别急着写代码,我们把这个需求翻译成人话:管理员定义一张表,里面有固定的标题、列头、说明文字、公式、下拉选项,普通用户在打开这张表时,只能往允许填写的白色格子里面录入内容,其余区域要么灰色、要么带锁定图标,双击没反应,键盘敲不进去。

这种场景在业务里到处都是。比如:

  • 车间巡检表:设备名称、巡检项固定,员工只需填“正常/异常”和备注;
  • 项目周报:指标体系固定,每人填写自己负责的进度列;
  • 经销商订货单:商品清单、单价、统一折扣由总部维护,经销商只填数量;
  • 医院科室登记表:字段固定,护士填写量表和观察项。

最关键的两个诉求其实是:一是不让用户误破坏模板结构,比如删掉列头、改掉公式;二是收集上来的数据格式可控,不能用户随便填个乱值就提交上来。

2.2 技术拆解:四层能力组合

要实现这个效果,不是某个 API 单独搞定的,而是四层能力协同:

能力作用对应 Univer 方案
模板定义固定表头、结构、公式快照 JSON / 初始化单元格数据
权限控制指定区域可编辑,其余锁定工作表保护 + 解锁范围
输入约束限制值域、格式、必填数据校验 DataValidation
UI 反馈让用户一眼知道哪里能填样式区隔、占位提示、只读视觉

每一层都不是万能的。模板定义只负责“长什么样”,权限控制负责“能不能改”,数据校验负责“改得对不对”,UI 反馈负责“好找、好用”。四层组合起来,那个“只可填指定格子”的体验才立得住。

2.3 一条铁律:前端锁定是体验,后端校验是底线

这一点必须说透。前端用保护、拦截做的锁定,本质是优化交互体验——让普通用户不容易误操作,让界面看起来更像一个“表单”而不是一张可以随便改的 Excel。但它挡不住真正懂技术的人:只要别人能打开浏览器开发者工具,理论上就能向 Univer 内部状态注入数据,甚至绕过前端直接请求后端接口。

所以业务上,如果这张“可填写的表格”最后要提交数据到服务端,后端必须对每一条字段做校验:字段名是否合法、值是否在枚举范围内、长度是否超限、该用户是否有权限提交这个区域的数据。前端锁定 + 后端校验,这套组合才算闭环。这点在团队内部做产品方案时一定要提前和后台同学对齐,不然后端只做了个“存 JSON”接口,上线后一定会收到脏数据。

3. 实操:基于 Univer 实现“模板表格 + 指定区域可填写”

3.1 环境准备与最小工程接入

先准备一个最基础的前端工程。我用的是 Univer 的 preset 快速接入方式,最适合从零开始跑通流程。实际安装时建议把版本锁死,因为 Univer 还处于快速迭代期,API 有变动。

npm install @univerjs/preset-sheets npm install @univerjs/core @univerjs/sheets @univerjs/sheets-ui @univerjs/sheets-data-validation

创建 HTML 容器,并初始化一个 Univer 实例:

<div id="app" style="width: 100vw; height: 100vh;"></div>
import { Univer, UniverInstanceType } from '@univerjs/preset-sheets'; const univer = new Univer({ // 主题、语言、环境配置,按自身工程需要调整 });

初始化时我先不加载具体数据,通过前面提到的快照方式,把模板数据注入进去。这里提醒一句:不同版本初始化 API 可能不同,以你安装版本的官方类型定义为准,我在下面给的方案是基于目前常用的 preset 接入方式。

3.2 第一步:用模板快照建立固定表头

最直接的方式是手写一份“模板快照”。拿月度巡检表举例,我会先在后台把表头、说明、公式这些固定内容用快照数据表达出来。

快速说明一下快照结构,大致长这样:

{ "id": "inspection-template-2025", "name": "车间月度巡检表", "sheetOrder": ["巡检表"], "sheets": { "巡检表": { "id": "sheet-inspection", "name": "巡检表", "rowCount": 60, "columnCount": 6, "cellData": { "0": { "0": { "v": "巡检项目" }, "1": { "v": "责任人" }, "2": { "v": "状态" }, "3": { "v": "备注" } }, "1": { "0": { "v": "设备 A 运行状态" }, "1": { "v": "张三" }, "2": { "v": "待填写" } } } } } }

不同版本快照的字段层级可能有差异,有的会在外层再包一层 workbook,有的直接用 createUnit 传 sheets 参数。建议不要在脑内记死结构,以 API 提示为准。但核心思路是一样的:先定义一张表长什么样,再执行加载。

加载快照到 Univer 实例:

univer.createUnit(UniverInstanceType.SHEET, snapshot);

加载完以后,模板已经出现在页面上了。此时所有单元格都是可编辑的,还需要进入权限控制环节。

3.3 第二步:工作表保护 + 解锁可编辑范围(推荐方案)

现在要在数据模型层面做“整表锁定,只放开部分格子”。

Univer 提供了工作表保护能力,原理和 Excel 里的保护工作表很类似:先给整个 sheet 加锁,再加一条“允许编辑范围”的白名单。白名单命中的单元格区域,即使工作表处于保护状态,用户也能正常编辑;不在白名单里的区域,用户双击、输入、粘贴都会被禁止。

通过命令服务执行保护操作:

import { ICommandService } from '@univerjs/core'; import { SetWorksheetProtectionCommand } from '@univerjs/sheets'; const commandService = univerAPI.getCommandService(); await commandService.executeCommand(SetWorksheetProtectionCommand.id, { unitId: 'inspection-template-2025', subUnitId: 'sheet-inspection', protection: { lock: true, enable: true, allowEditRanges: [ { name: '允许填写状态', range: { startRow: 1, startColumn: 2, endRow: 30, endColumn: 2 } }, { name: '允许填写备注', range: { startRow: 1, startColumn: 3, endRow: 30, endColumn: 3 } } ] } });

我实际用下来的体会是,保护命令一定要放在模板数据加载完成之后触发,而且最好在“模板发布”这个动作里统一执行,不要散落在各种页面初始化逻辑里。否则每个入口都要处理一遍,很容易漏。

设置完成以后,用户看到这张表时,除了“状态”“备注”两列对应的范围内,其他单元格都不可编辑。

3.4 第三步:用事件拦截做二次兜底(可选)

工作表保护已经解决了绝大部分问题,但有些场景我更建议再叠一层“交互拦截”:比如你希望某些单元格虽然在保护白名单里,但仍然不允许临时填错;或者你担心某些界面入口绕过保护逻辑。这时可以监听命令流水。

Univer 的命令对象在真正执行前,会经过 beforeCommandExecute 事件。我们可以在里面拦截编辑类命令,做更细的判断。

import { ICommandService } from '@univerjs/core'; import { SetRangeValuesCommand } from '@univerjs/sheets'; const commandService = univer.getCommandService(); commandService.beforeCommandExecute((command) => { if (command.id === SetRangeValuesCommand.id) { // 检查当前选区是否都在允许编辑范围内 const params = command.params as { ranges: Array<{ startRow: number; startColumn: number; endRow: number; endColumn: number }> }; const allow = params.ranges.every((range) => isInAllowedRange(range)); if (!allow) { // 阻断命令执行 return false; } } return true; }); function isInAllowedRange(range: { startRow: number; startColumn: number; endRow: number; endColumn: number }) { // 白名单逻辑,比如只允许 C 列和 D 列的第 2~30 行 const allowStartRow = 1, allowEndRow = 29; return range.startColumn >= 2 && range.endColumn <= 3 && range.startRow >= allowStartRow && range.endRow <= allowEndRow; }

注意,返回false是目前常用的阻断方式,具体字段名和命令 id 请以你版本的源码为准。事件拦截更适合做细粒度、临时性的控制,比如某个用户在某个时段内只能改哪几行。如果是长期固定的权限规则,还是建议回到保护方案,否则一旦命令生命周期调整,拦截代码也要跟着维护。

3.5 第四步:给可填写单元格加上数据校验

允许填写不代表能随便填。Univer 支持单元格数据校验,比如限制只能填数字、只能从下拉列表里选一个值、日期格式必须正确等。拿巡检状态列来说,最好放一个下拉列表,让用户只能选“正常、异常、待复检”三个值。

通过数据校验命令给指定范围添加校验规则:

import { SetRangeDataValidationCommand } from '@univerjs/sheets-data-validation'; import { DataValidationType } from '@univerjs/sheets-data-validation'; await commandService.executeCommand(SetRangeDataValidationCommand.id, { unitId: 'inspection-template-2025', subUnitId: 'sheet-inspection', range: { startRow: 1, startColumn: 2, endRow: 30, endColumn: 2 }, rule: { type: DataValidationType.LIST, formula1: '"正常,异常,待复检"', allowBlank: false, showDropDown: true, errorStyle: 'STOP' } });

设置完以后,用户选择“状态列”任意一个单元格,右侧或下方会出现下拉箭头。输入值如果不在枚举范围内,Univer 会给出错误提示并阻止提交。

我建议设计模板时把校验规则也视为模板的一部分。这样可以做到“管理员改规则,用户自动生效”。你可以在给模板做版本管理时,把校验规则一并放进快照元数据里。

3.6 一个完整的示例:月度巡检填报表

我们把上面的步骤串起来,做一个完整案例。假设场景是这样的:车间有 8 个巡检设备,每个设备需要填写“运行状态”和“备注”。表结构为 A 列固定写设备名,B 列固定写责任人,C 列给用户填状态,D 列给用户填备注。其他任何区域都不可编辑。

完整操作流程如下:

  1. 构建模板快照,在 A 列和 B 列写入固定内容;
  2. 对第 1 行写表头,固定合并单元格、设置底色;
  3. 加载模板到 Univer;
  4. 执行工作表保护,允许编辑范围设为 C2:D9;
  5. 给 C2:C9 添加下拉校验,值为“正常、异常、待复检”;
  6. 给 D2:D9 添加文本长度校验,限制最长 100 字。

关键部分在一个函数里统一切割:

async function publishInspectionTemplate() { // 1. 创建快照 univer.createUnit(UniverInstanceType.SHEET, buildInspectionSnapshot()); // 2. 保护工作表,只放开 C2:D9 await commandService.executeCommand(SetWorksheetProtectionCommand.id, { unitId: 'inspection-template-2025', subUnitId: 'sheet-inspection', protection: { enable: true, allowEditRanges: [ { name: '状态区域', range: { startRow: 1, startColumn: 2, endRow: 9, endColumn: 2 } }, { name: '备注区域', range: { startRow: 1, startColumn: 3, endRow: 9, endColumn: 3 } } ] } }); // 3. 加数据校验 await commandService.executeCommand(SetRangeDataValidationCommand.id, { unitId: 'inspection-template-2025', subUnitId: 'sheet-inspection', range: { startRow: 1, startColumn: 2, endRow: 9, endColumn: 2 }, rule: { type: DataValidationType.LIST, formula1: '"正常,异常,待复检"', allowBlank: false, showDropDown: true } }); }

跑完这个函数,用户打开页面时:表头有底色,固定区域是灰色,只有 C2:D9 的格子能够编辑,而 C 列必须从下拉里选值。整体体验非常接近一个“表单”,而不是一张任人修改的表格。

4. Univer 实战中的常见问题与避坑实录

4.1 问题速查表

我在自己项目里遇到过不少问题,整理成一张速查表,方便大家照着排。

问题现象可能原因处理方式
保护了工作表但自己也无法编辑allowEditRanges 没设置或范围算错了检查行列号起点是 0 还是 1,确认白名单范围覆盖目标区域
用户双击单元格提示“受保护”但视觉上不明显锁定区域没有和可编辑区域做样式区分模板中给锁定单元格设置灰底、斜纹或边框,引导用户
下拉校验弹不出来校验命令的 unitId/subUnitId 与当前活动表不一致打印 console 日志确认当前活动表 id,与快照中的 id 对齐
快照加载后模板显示空白快照结构字段名不对,比如 sheets 层级不匹配用官方示例 JSON 对比字段,逐个字段排查,不要靠猜
公式填写后不计算公式写法或区域引用错误,或缺少计算引擎插件使用 Univer 的标准公式语法,检查插件是否注册完整
事件拦截不生效命令 id 变了,或拦截注册太晚查看当前版本命令常量名,最好在插件初始化阶段注册
数据校验提示不够友好使用的是默认错误气泡自定义校验提示文案,或在单元格备注里预先写明填写要求
协同场景下同一范围被多人同时改权限只做了静态配置,没做交互锁需要后端介入,做行级或单元格级锁/合并冲突策略

4.2 我用下来最有价值的几条经验

先把版本锁死。Univer 现在迭代速度很快,API 改名是常事,如果不锁版本,固定工作流代码跑着跑着突然失效,排查成本很高。我的做法是 package.json 里直接锁版本号,升级时单独看 changelog。

再就是不要把核心逻辑散在组件里。我一开始图方便,在 React 组件 useEffect 里又创建实例又发保护命令,后来发现组件一重发,命令重复执行,很容易出竞态问题。最好做一个独立的“表格服务”模块,把创建实例、加载模板、设置保护、设置校验都封装成一个异步函数,页面只管调用和销毁。

第三个经验是关于“视觉提示”。不要以为保护锁定了就万事大吉。用户打开一张表,如果看不出哪里能填,会非常困惑。我在模板里做了三个提示:可编辑区域白色背景,锁定区域浅灰背景,表头深色背景;状态列的未填空单元格里预先写入“待填写”三个字,用户一编辑就自动替换;表格顶部加了一个说明行,直接写“请只填写 C 列和 D 列”。这三件事配合起来,几乎不需要额外培训用户。

4.3 一个容易踩的坑:行列号起点

Univer 快照和命令里的行列索引在大多数版本里是 0 起始的,第 1 行就是 index 0,第 A 列就是 column 0。这意味着,用户看到的第 2 行第 3 列(C2),在代码里对应的是{ startRow: 1, startColumn: 2 }。

这个坑几乎每个人都会踩。如果你用数据校验时发现范围总差一行、差一列,先怀疑这里的索引偏移。我建议封装一个辅助函数,把“用户视角的行列号”转换为“代码视角的索引”,统一处理。比如:

function toZeroIndex(row: number, column: number) { return { row: row - 1, column: column - 1 }; }

这样就不需要每个命令里反复人肉减一了,也方便以后代码 review 一看就懂。

5. 延伸:把 Univer 做成业务系统的表格引擎

5.1 从“单张表”到“表单引擎”

一旦你掌握了“模板快照 + 工作表保护 + 数据校验”这套组合,思路就可以打开。它不仅仅是做一张巡检表,而是能做成一个完整的表格引擎:

  • 管理员端提供“模板编辑模式”:加载原始模板,用 Univer 自己的 UI 随意改表头、改公式、加校验,保存时生成新的快照;
  • 发布时把快照存到数据库,同时记录模板版本号、发布人、发布时间;
  • 用户端加载最新版快照,按模板里的保护规则进入“只可填写指定区域”的模式;
  • 用户提交后,后端保存数据并把该用户的填写结果回填到快照副本里,保留历史的每一次填报版本。

这样就用一套技术栈覆盖了“模板设计、模板发布、数据采集、历史追溯”的完整链路。比起传统用 Grid 组件一个格子一个格子拼 UI,Univer 的好处是模板天然就是表格,设计成本极低。

5.2 填写数据的提取与回写

工程上经常需要考虑:用户填写后,如何把那些可编辑区域里的数据拿出来?可以在提交时遍历白名单范围,从 Univer 的数据模型里读取值。

大致思路如下:

function collectEditableDataFromSheet(sheet) { const allowRanges = [ { startRow: 1, startColumn: 2, endRow: 9, endColumn: 2 }, { startRow: 1, startColumn: 3, endRow: 9, endColumn: 3 } ]; const data = {}; for (const range of allowRanges) { for (let r = range.startRow; r <= range.endRow; r++) { for (let c = range.startColumn; c <= range.endColumn; c++) { const cell = sheet.getCell(r, c); data[`${r}-${c}`] = cell ? cell.v : null; } } } return data; }

这些结构化数据提交给后端后,后端需要做两件事:一是校验每一条数据是否符合模板对应的字段规则;二是把数据按用户维度存起来,下次打开模板时,如果有历史值,可以直接回填到对应单元格,让用户改起来更方便。

5.3 更多可玩的方向

Univer 还能做不少高阶玩法,挑几个我觉得最有潜力的说:

  • 多人协同:它支持协同编辑能力,可以做一套“多用户同时填一张表”的业务。此时权限控制就更加重要,最好配合服务端做单元格级别的锁。
  • 自定义单元格渲染:你可以在表格里渲染图片、状态标签、进度条,甚至嵌入一个按钮。对数据采集场景来说,状态列可以渲染成彩色圆点,视觉反馈远比纯文本强。
  • 公式联动:管理员可以在模板里预置汇总公式,比如“异常数量 = COUNTIF(C2:C9, '异常')”,用户在填写时,统计结果实时变化。这个能力是普通表单组件很难有的。

我个人的体会是,Univer 这套体系的价值不在于“做一个好看的表格”,而在于把表格重新变成一个可以被业务逻辑驱动的数据载体。你要做的不是复制它的 UI,而是利用它的数据模型、命令系统、渲染扩展点,把它嵌到你的业务流程里。先跑通“模板 + 保护 + 校验”这条最小链路,往后做协同、做提交、做审批就都有基础了。

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

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

立即咨询