☰
Univer 在线表格单元格锁定:实现指定区域可编辑的完整指南
2026/10/2 16:05:18 网站建设 项目流程

“想用在线表格做一个报名表或者工单登记表,业务同学打开网页就能填,但只能改我指定的那几格,标题、说明,还有那些公式列,一概不能碰”——这是我最近被问到次数最多的一个需求。字面听上去不复杂,真正落到代码里才发现弯弯绕绕不少。如果你正在用Univer搞这类“用户定义表格 + 指定单元格填写、其他单元格锁定”的在线表格场景,或者刚接触 Univer 在评估它能不能扛起这个业务,这篇就是给你准备的实操笔记。我会从项目选型讲到权限模型,再给可直接抄的配置和代码,最后整理几个我实际踩过的坑。

1. Univer 是什么:项目定位与核心技术拆解

1.1 开箱即用的在线表格内核

Univer 是一套基于 TypeScript 构建的开源办公套件方案,主打在线创建、编辑和预览电子表格、文档与幻灯片。如果你听过 Luckysheet,那可以把它理解为 Luckysheet 团队在架构上推翻重做后的下一代产物:渲染层换成了 Canvas 绘制,核心与 UI 彻底解耦,插件化程度更高,还内置了一套公式引擎和命令系统。

从业务接入角度,Univer 最让人舒服的一点是:它不是给你一个“静态表格组件”,而是给你一个完整的、可编程的表格运行时。你可以在里面定义工作簿、工作表、单元格数据、公式、样式、数据校验,也可以通过命令服务去执行“设置单元格值”“合并单元格”“开启工作表保护”这些操作。换句话说,你不需要像过去那样在一堆 div 和 table 标签上模拟表格行为,而是直接操作一个真实存在的电子表格实例。

Univer 的模块划分大概是这样:

  • @univerjs/core:核心数据模型,工作簿、工作表、单元格、区域、样式、命令框架都在这层。
  • @univerjs/sheets:表格业务逻辑,包括单元格编辑、选区、公式计算、筛选、排序等基础能力。
  • @univerjs/sheets-ui:表格 UI 层,负责渲染工具栏、编辑栏、右键菜单、弹窗。
  • @univerjs/ui:通用 UI 基础设施,让 Univer 可以嵌入 React、Vue 或原生 JS 项目。
  • @univerjs/engine-formula:公式引擎,用来处理跨表、跨工作簿的公式计算。
  • @univerjs/engine-render:Canvas 渲染引擎,负责把表格画到页面上。

这个分层直接决定了它的扩展性。比如你只想用表格编辑能力,不想显示官方那一整条工具栏,那完全可以不注册 UI 插件,自己写一套编辑入口;反过来,如果你需要标准 Excel 体验,把 sheets-ui 注册上就基本齐了。正因为这种灵活度,Univer 特别适合做“半定制化”的业务表格,而不是只能全盘照搬的标准 Excel。

1.2 为什么选 Univer 做“用户填表”场景

做“用户填写指定单元格”这件事,传统方案一般有三条路:直接发 Excel 模板让人填了再回收、用传统前端表格组件仿一个填表页、或者直接在自己系统里做表单引擎。这三条路各有各的别扭:

  • Excel 模板回收,版本混乱、格式被改、收集汇总全靠人肉,体验很差。
  • 前端表格组件仿填表页,表格行为很难做到位。用户想要拖动填充、下拉选择、公式联动时,基本都要自己造轮子。
  • 表单引擎虽然能限制输入项,但表达不了表格布局。比如一行一个项目的工时填报,或者横竖轴交叉的排班表,用表单控件排出来非常痛苦。

Univer 刚好卡在中间:它有原生表格的交互能力,又允许你用编程方式控制哪些单元格可编辑、哪些被锁定。管理员先在页面上把模板搭好,锁定不需要用户碰的区域,再把链接发给用户,用户在网页里只能按预定位置填写。整个流程在线上闭环,数据直接回传后端,既避免了 Excel 文件满天飞,又保留了表格天然的布局表达能力。

更关键的是,Univer 的保护机制不是“只能设置整表只读”这种一刀切,它支持把工作表的“保护”和“单元格的锁定属性”拆开组合,配合非常细的权限范围,能够准确实现“某些区域可以编辑、其他区域不能改”的需求。这就是这篇实操里最核心的切入点。

2. 需求拆解:让用户填写指定单元格,其余锁定

2.1 “用户定义表格”的业务本质

先把“用户定义表格”这个说法拆开。用户这个词在不同场景里指代不一样。在多数业务系统里,设计表格模板的是管理员或财务,最终填写数据的是普通员工或外部客户。所以“用户定义表格”实际上包含两层含义:

  1. 模板定义权:谁来创建表格结构、设置标题、公式、校验规则、锁定规则。
  2. 数据填写权:谁能在特定区域内填入内容。

这篇文章要解决的核心是第二层,但实现第二层之前,必须先想清楚第一层。因为模板的定义过程往往也需要在 Univer 里面完成,如果管理员自己都分不清哪些单元格是锁定用的、哪些是放开用的,后面所有规则都是空中楼阁。

一个典型的业务例子是培训报名表:

  • A1:D1 是合并标题“2025年第三期安全培训报名表”。
  • 第二行是列名:姓名、部门、邮箱、是否住宿。
  • 管理员不希望用户改标题和列名,甚至不希望用户能选中这些单元格;
  • 用户只需要从第三行往下填写自己的信息;
  • 如果邮箱格式错了,表格应给出提示。

这个例子里的“可编辑区域”就是一个从第三行到表格末尾的数据区域。用户在这个区域里输入内容,其他区域要么锁定、要么只读。管理员创建模板时,也应该把这个规则体现在配置里,而不是等表格上线后再去临时设置。

2.2 权限模型与可编辑范围控制的关键点

Univer 控制可编辑性的机制,本质上沿用了 Excel 那套经典的“工作表保护 + 单元格锁定”模型。

先记住一个关键结论:单元格默认的锁定状态,并不等于用户不可编辑。只有当工作表开启了保护(protection)之后,锁定属性才会生效。这个关系可以类比成小区门禁:每个房间有门锁(单元格 locked 状态),但只有保安启动门禁系统(工作表保护),这些门锁才真正起作用。如果你只给每个房间换了锁,却让保安放假,那谁都能推门进去。

在 Univer 的配置模型里,工作表保护对象至少包含这几个关键字段:

  • sheet:布尔值,表示这张工作表是否启用保护。
  • lockCells:布尔值,表示当前工作表是否锁定所有单元格。
  • ranges:数组,用来声明保护范围内的例外区域,每个区域可以单独设置是否允许锁定。

当lockCells: true且protection.sheet: true时,整张表默认不可编辑,只有ranges里明确列为 unlock 的区域可以编辑。反过来,如果lockCells: false,整张表默认可编辑,ranges里的区域可以被单独锁死。大多数“用户填表”场景用的是前者:先锁全表,再把填写区域放出来。

还有两个容易被忽略的配置项:allowSelectingLockedCells和allowSelectingUnlockedCells。前者控制用户能不能点选锁定区域,后者控制用户能不能点选可编辑区域。如果业务上要求“用户连标题都选不中”,就把allowSelectingLockedCells设为 false;如果允许用户点选已填写的内容,只是不能修改,那就保持为 true。我这边的经验是,填表业务里通常把两个都放开,因为用户选中有助于看清他填过什么,只要不能编辑就可以了。

另外要注意,Univer 的保护模型是工作表级别的,不是工作簿级别。如果你想整个工作簿都进入“填表模式”,需要遍历里面每一张工作表,分别设置保护。如果有多个 Sheet,且用户应该只能看到其中一张填报表,那更实用的做法是直接隐藏其他工作表,只保留目标表。

3. 实操:在 Univer 中实现“可指定区域填写”

3.1 环境准备与最小示例

先搭一个最小可运行的 Univer 项目。我用的是 Vite + TypeScript 的 React 工程,其实框架不限,Univer 官方封装好了 React 组件和非 React 接入两种方式,核心逻辑一样。

安装依赖:

npm install @univerjs/core @univerjs/sheets @univerjs/sheets-ui @univerjs/ui @univerjs/engine-formula @univerjs/engine-render

初始化代码大概长这样:

import { Univer } from '@univerjs/core'; import { UniverSheetsPlugin } from '@univerjs/sheets'; import { UniverSheetsUIPlugin } from '@univerjs/sheets-ui'; import { UniverUIPlugin } from '@univerjs/ui'; import { UniverFormulaEnginePlugin } from '@univerjs/engine-formula'; const univer = new Univer({ locale: 'zhCN', }); univer.registerPlugin(UniverSheetsPlugin); univer.registerPlugin(UniverFormulaEnginePlugin); univer.registerPlugin(UniverUIPlugin, { container: 'app', }); univer.registerPlugin(UniverSheetsUIPlugin);

这一步跑起来以后,页面右上角会出现 Univer 自己的工具栏。我建议开发阶段不要急着隐藏工具栏,因为“保护工作表”这个功能在工具栏里可以直接点,方便你验证效果。生产环境再根据业务隐藏不必要按钮。

然后在项目里创建一张工作表数据。Univer 创建表格时可以传一个类似“工作表配置快照”的对象,里面包含单元格数据、样式、合并信息、行高列宽,还有我们关心的保护配置。下面的示例是完整模板结构:

const formSheet = { id: 'training-signup', name: '报名填写', rowCount: 20, columnCount: 6, cellData: { 0: { 0: { v: '2025年第三期安全培训报名表', s: { bl: 1, bg: '#f2f2f2', locked: true, merge: 3 } }, }, 1: { 0: { v: '姓名', s: { locked: true, bg: '#e8e8e8' } }, 1: { v: '部门', s: { locked: true, bg: '#e8e8e8' } }, 2: { v: '邮箱', s: { locked: true, bg: '#e8e8e8' } }, 3: { v: '是否住宿', s: { locked: true, bg: '#e8e8e8' } }, }, }, protection: { sheet: true, lockCells: true, allowSelectingLockedCells: true, allowSelectingUnlockedCells: true, ranges: [ { range: { startRow: 2, endRow: 19, startColumn: 0, endColumn: 3 }, lock: false, }, ], }, }; univer.createSheet(formSheet);

这段配置的作用很直白:把整张表锁定,然后在第 3 行到第 20 行的前四列放出一个可编辑区域。用户在网页上打开后,表头区域内容灰色、选中但改不了;第 3 行及以下的白色区域内,用户可以像操作 Excel 一样输入姓名、部门、邮箱和住宿信息。

3.2 定义可填写区域:模板配置方式

上一步的 protection 配置里,ranges就是指“放给用户编辑的区域”。这个数组可以包含多个不连续的区域,比如一张表上既有“基本信息区”又有“家庭成员区”,那就写两个 range 对象。注意startRow和endRow、startColumn、endColumn都是从 0 开始计数的,表格第 1 行对应 startRow 0,第 1 列对应 startColumn 0。这个计数方式特别容易踩坑,我第一次写就把第三行写成了 startRow 2 还是 3 纠结了半天。

单元格样式里的locked是给单元格本身打标用的。如果你在创建模板时想明确某个单元格“永远不能被编辑”,除了在 protection 的 ranges 里不放这个区域,还可以同时在s.locked上做标记。但需要说明:locked 只是标签,保护范围才是最终执行依据。开启保护后,Univer 执行编辑命令时会去检查“当前选中区域是否在 protection 的允许编辑范围内”。所以模板正确性最终看 protection.ranges,单元格的 locked 属性用来配合 UI 展示,比如给锁定区域加浅灰背景,让用户一眼看出哪里不能填。

3.3 运行时切换编辑权限:命令方式

模板配置是“静态初始化”的做法,适合表格结构在代码里写死。但真实业务里,管理员很可能要在界面上临时修改可编辑范围,这就需要用命令动态调整保护配置。

Univer 的命令服务(CommandService)是运行时改变表格状态的唯一正规入口。示例代码如下:

import { ICommandService } from '@univerjs/core'; import { SetWorksheetProtectionCommand } from '@univerjs/sheets'; const commandService = univer.getCommandService(); await commandService.executeCommand(SetWorksheetProtectionCommand.id, { unitId: 'training-signup', subUnitId: 'training-signup', protection: { sheet: true, lockCells: true, ranges: [ { range: { startRow: 2, endRow: 19, startColumn: 0, endColumn: 3 }, lock: false }, { range: { startRow: 2, endRow: 19, startColumn: 4, endColumn: 5 }, lock: true }, ], }, });

这里有两个 id:unitId是工作簿的 id,subUnitId是工作表的 id。在我这个例子里两者都用了同一个字符串。如果你的业务里有两张 sheet,那 subUnitId 就分别指向对应 sheet。很多初学时困惑的“为什么执行命令没反应”,八成是这两个 id 没对上。调试时可以先打印工作簿和工作表的 id:

const workbook = univer.getActiveWorkbook(); const worksheet = workbook.getActiveSheet(); console.log(workbook.getId(), worksheet.getId());

执行完这个命令后,表格的编辑权限会立即变化,不需要刷新页面。这种动态控制很适合做“审批流”:管理员编辑模板时保护未开启,审核通过后调用命令开启保护,业务用户拿到链接就进入了填写模式。

3.4 前端纯拦截兜底方案

保护机制原理上是拦截了 Univer 内部的编辑命令,但并不是所有交互都能被 protection 覆盖。比如某些版本里,你仍然可以通过填充柄向下拖拽一个锁定区域的值,或者复制锁定区域粘贴到可编辑区域。遇到这种情况,光靠 protection 不够,还得在前端事件层做兜底。

Univer 提供命令执行监听,我们可以拦下不必要的操作:

univer.getCommandService().onCommandExecuted((command) => { if (command.id === 'sheet.command.set-range-values') { // 检查 command.params 里的 range 是否落在可编辑区域内 // 如果不在保护范围内,就拦截或者回滚 } });

更简单的做法是在编辑器外层加一层业务校验:拿到用户提交数据后,在后端再次校验“提交的字段是否都位于允许范围内”。前端保护是为了体验,后端校验才是底线。这个原则放在任何表格权限场景都适用。

4. 进阶:协同填写与后端保存的完整方案

4.1 多用户同时填写时保护区域怎么保证

如果你只是把一张带保护的工作表发给用户,每个用户独立打开、独立填写,那权限控制很清晰。但现实里经常出现几十个人同时打开同一张表,各自往自己那一行填数据。Univer 本身支持协同编辑(底层可以用 WebSocket、Yjs 等同步),但协同模式下保护逻辑的复杂度会上升。

首要原则是:不要把后端权限校验寄托在前端保护上。前端保护只是 UI 层面的限制,协同服务收到操作指令后,必须自己校验这个 range 是否允许写入。否则一个懂点前端的人,可以绕过界面直接调协同同步接口,把锁定区域的数据改掉。

具体操作层面,我建议把“保护配置”提升为后端的一张配置表:这张表里存了工作簿的 unitId、sheet 的 subUnitId、允许编辑的 ranges 列表。用户发起编辑时,协同服务先查配置表,校验操作范围再决定是否放行。如果业务里用到了 Univer 官方协同方案,可以基于其命令广播机制,在命令进入同步管道之前加一个鉴权中间层。

4.2 把填写结果持久化到后端

填表业务最终要落库。Univer 里读取用户填写内容有几种做法:

  1. 用户填完后,前端统一从表格实例中取出整个数据区。
  2. 监听单元格变更事件,实时增量提交。

第一种做法适合“填完点提交”的流程。代码大概这样:

const worksheet = univer.getActiveWorkbook().getActiveSheet(); // 读固定区域的数据 const rangeData = worksheet.getRange({ startRow: 2, endRow: 19, startColumn: 0, endColumn: 3, }); const rows = rangeData.map(row => ({ name: row.cells?.[0]?.v, department: row.cells?.[1]?.v, email: row.cells?.[2]?.v, accommodation: row.cells?.[3]?.v, }));

然后把这组对象 POST 到后端接口。这里的重点是:读取数据时不要读全表,只读你允许填写的区域,既减少不必要的传输,也天然规避了越权数据被带上来的风险。

第二种做法适合表格长期打开、自动保存的场景。Univer 的命令服务有对应事件:

univer.getCommandService().onCommandExecuted((command) => { if (command.id === 'sheet.command.set-cell-value') { // 把 command.params 里的 values 增量提交 } });

增量提交要做防抖,不然用户连续输入十几个字符会打出十几条请求。我习惯把变更先缓存到一个 Map 里,用 500ms 的定时器统一上报。

4.3 配合表单校验与数据联动

“能编辑”和“能填对”是两回事。用户虽然只能写指定区域,但写出来的内容可能格式完全不对。Univer 在填表场景下最好开启数据校验能力。

比如邮箱列,可以在模板配置里给单元格加上校验规则,或者使用公式做判断。Univer 支持在初始化时给单元格指定 validator;在填表业务中,更实用的做法是监听值变更,在 UI 上实时提醒。这里有一个经验:校验规则不要写在保护配置里,也不要散落在模板各处,最好集中在一个数据字典结构里带进模板,这样后端校验和前端提示共用同一份规则,避免两边不一致。

举个例子,邮箱列的校验规则可以定义为:

{ type: 'regex', pattern: '^[\\w.+-]+@[\\w-]+(\\.[\\w-]+)+$', message: '邮箱格式不正确', }

用户填完不合法,提交按钮置灰;只有全部合法才能提交。这提升了表格的可用性,也让后台少收很多脏数据。

5. 常见问题与避坑指南

5.1 保护开启后锁定区域仍能编辑

这是我在社区里看到最多的问题,自己第一次也遇到。排查顺序如下:

  • 确认 protection 的sheet是否真的为 true。很多人只设置了lockCells,没有把sheet打开,保护等于没启用。
  • 确认lockCells是否为 true。如果 lockCells 为 false,ranges 之外的区域默认可编辑,保护范围的含义反过来了。
  • 确认 ranges 的lock字段。可编辑区域要用lock: false显式标记。有些版本字段名是locked,拿到的示例代码里写的是lock就照抄,结果毫无反应。
  • 确认执行的是重新设置整套 protection,而不是增量 patch。Univer 命令执行时通常会整体替换 protection 对象,所以每次更新都要把完整的 ranges 放进去。

5.2 公式计算、填充柄绕过保护

保护只能拦编辑命令,拦不住用户把可编辑区域的公式向下填充到锁定区域。如果想彻底避免这种问题,有两个思路:

  • 在锁定区域不上公式,改由后端统一计算。
  • 在事件层拦截填充操作,只允许在非锁定区域范围内执行。

第二种思路实现起来要监听比较底层的命令,比较麻烦。我的建议是:填表场景能不用公式就不用公式。表格的计算能力让管理员在后台设计模板时用,最终提交给用户的填表视图,尽量只展示普通文本和数字,把公式计算挪到保存后的结果页。

5.3 Excel 导入与保护兼容性问题

Univer 支持导入 xlsx,但导入文件的保护配置不一定能完整还原。Excel 里“允许用户编辑区域”是通过范围安全性设置的,Univer 从 xlsx 里解析时可能会丢失或者转换偏差。如果业务要求管理员先上传 Excel 模板,再由系统启用填表模式,我的建议是:上传后不要依赖原文件的保护属性,而是按业务规则重新生成 protection ranges。

怎么做呢?上传后先解析 Excel 的单元格结构,然后通过规则匹配出可编辑区域。比如约定“所有带黄色背景的单元格为可编辑区域”,解析时读单元格背景色,把这些坐标转成 ranges。这样做的好处是模板设计者不需要懂 Univer API,只要在 Excel 里涂色就行。

5.4 大表格初始化性能

填表模板一般不会太大,但如果管理员从 Excel 导入了几千行数据,Univer 初始化时全量渲染会卡顿。优化手段有几个:

  • 减少初始 cellData 里所有单元格都赋空对象的情况,Univer 对稀疏数据渲染更友好。
  • 隐藏不必要的行列,不要让用户看到空白区域。
  • 条件格式和校验规则不要铺满整张表,只设置在真正的数据区域。
  • 如果可编辑区域很单调,优先用 range 规则代替逐格样式,尽量减少单元格级对象数量。

6. 一套可直接使用的“可填写表格”配置参考

6.1 模板配置速查

最后给一份完整的、可以改改就用的配置。我用“工时统计”举例,管理员每月发一张表给组员填写,组员只能填“项目名称、工时、说明”三列,其他列锁定。

工作表规划:

  • 第 1 行:合并标题。
  • 第 2 行:列名:项目编号、项目名称、工时、说明。
  • 第 3 行到第 20 行:填写区。
  • 项目编号列锁定,内容由系统写入;项目名称、工时、说明列可编辑。

配置如下:

const timesheet = { id: 'timesheet-2025-06', name: '6月工时', rowCount: 20, columnCount: 4, cellData: { 0: { 0: { v: '2025年6月工时登记表', s: { bl: 1, bg: '#f2f2f2', locked: true } }, 1: { v: '', s: { locked: true } }, }, 1: { 0: { v: '项目编号', s: { locked: true, bg: '#e8e8e8' } }, 1: { v: '项目名称', s: { locked: true, bg: '#e8e8e8' } }, 2: { v: '工时', s: { locked: true, bg: '#e8e8e8' } }, 3: { v: '说明', s: { locked: true, bg: '#e8e8e8' } }, }, }, protection: { sheet: true, lockCells: true, ranges: [ { range: { startRow: 2, endRow: 19, startColumn: 1, endColumn: 3 }, lock: false, }, ], }, };

用户打开后,项目编号列由系统预先填好,用户只能填项目名称、工时、说明三列。这样收集上来的数据非常规整,后端解析也方便。

6.2 事件联动:填写完成后的处理

一个比较好用的小技巧是,监听单元格变更之后,把变更单元格标成其他背景色,这样用户一眼就能看出自己填了哪些格子,管理员也能快速判断哪些数据是新增的。核心代码就几行:

univer.getCommandService().onCommandExecuted((command) => { if (command.id === 'sheet.command.set-cell-value') { const { unitId, subUnitId, values } = command.params; // 遍历 values,把对应单元格背景色置为浅绿或浅黄 } });

要提醒一句:这个监听事件非常频繁,做样式更新时最好合并批处理,避免每输入一个字符就重绘一次。我一般把待更新格子攒到一个数组里,等事件循环空闲时统一应用。

这个“可填写区域控制”的功能,真正的关键不在 API 调用,而是把权限模型想清楚。前端保护做得再好,也只是给用户一个顺畅的操作边界,后端必须持有同一份规则做最终校验。我在实际项目里被坑最惨的一次,就是前端保护全做好了,协同接口漏了校验,结果用户直接绕过界面改掉了锁定的公式列。后来我把 ranges 配置抽成公共模块,前后端共用,问题才彻底消失。你动手做的时候,建议第一步就先设计好这份公共配置,再碰 Univer 的代码,后面会省掉非常多的返工。

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

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

立即咨询