☰
基于Univer的在线表格模板化数据收集:单元格锁定与权限控制全攻略
2026/10/2 3:45:18 网站建设 项目流程

Univer 这个名字,做前端的人这两年应该不陌生了——它是一个用 TypeScript 写的开源在线表格/办公套件组件,可以像普通组件一样嵌进你自己的 Web 项目,实现类似在线 Excel 的完整交互。我最初接触它,是因为一个特别具体、也特别常见的需求:系统里要支持"管理员先定义一张表格模板,然后让用户填写指定的某些单元格,其他单元格一概不能改"。这个需求说白了就是表单收集场景,但用户又不想要传统表单那种僵硬布局,希望保留表格的灵活性和 Excel 的使用习惯。这篇文章会把我从选型到上线的完整过程拆开讲,包括为什么用 Univer、怎么设计"可填写 + 锁定"两类单元格、权限怎么控制、数据怎么收回来,以及实际踩过的坑。适合正在做内部数据收集、报销审批、信息登记、问卷填报这类功能的前端同学,也适合想给管理系统快速嵌入在线表格能力的团队参考。

1. 这个需求到底在解决什么问题

1.1 最典型的几个使用场景

做企业管理系统的朋友应该一秒就能对上号。我这边遇到的需求是"模板化数据收集":让管理员先画出一张表,把标题、固定列、公式、默认值都填好,然后分发给参与人填写属于自己的那一格。类似的场景在很多产品里都能看到:

  • 报销单:费用项目、金额、票据张数由员工填,税率、合计、审批意见不能动。
  • 项目周报:本周进展、风险、下周计划可填,项目名、负责人、截止日期锁死。
  • 客户信息登记:一线人员在统一字段表里维护,但表头、说明列、选项列不能改。
  • 调研问卷:用表格形态做收集,比普通表单更紧凑,还能直接引用公式做汇总统计。

这些需求有一个共同点:既想要表格的多字段、行列对齐、公式自动计算,又想要表单的"只能填该填的"强约束。如果只做一个传统 HTML 表单,字段固定、布局死板;如果放任大家直接在 Excel 文件里填,头天发出去的模板,第二天就有人把公式删了或者把表头改了,数据收回来还得花大量时间清洗。

1.2 传统方案为什么很别扭

在决定用 Univer 之前,我认真盘过几条常规路线,各有各的硬伤。

第一是纯表单方案。字段一变就要改前端代码,加一个新枚举值要重新发版;遇到需要公式联动、合计自动计算的场景,前端还得自己写一套计算逻辑,维护成本非常高。第二是Excel 文件传来传去。这是业务上最常见的土办法,但版本管理基本靠"文件名加日期",多人同时编辑时互相覆盖,回收后格式错乱、内容填错位是常态。第三是自己封装一个表格控件。听着可控,实际做起来发现要把单元格样式、合并、校验、锁定、公式这些能力全做齐,工作量足以让一个团队干上一个季度。

所以我当时的判断是:这个场景需要一个"自带表格内核"的组件,我不需要从零造轮子,而是把精力放在业务约束上——哪些格子能填、哪些不能填、填完后怎么收数据。

1.3 Univer 正好切中要害

Univer 的出现让我这套想法有了落点。它本身是 MIT 协议的开源项目,核心定位就是"把办公能力变成一个个可注册的插件",嵌入成本很低。对我要做的场景来说,它有几个能力刚好是刚需:

  • 完整的表格交互:公式、条件格式、数据校验、合并单元格、冻结窗格都内置。
  • 单元格锁定与工作表保护:这正是"其他单元格用户无法修改"的实现基础。
  • 支持 xlsx 导入导出,方便把模板从 Excel 迁移到线上,收完数据还能导回 Excel 归档。
  • 渲染走 Canvas,大数据量下比 DOM 表格稳定得多。
  • 社区活跃、版本迭代快,官方文档和示例都在持续更新。

我也简单对比过其他几个开源方案:SheetJS 主要做数据解析和导出,没有交互式编辑 UI;x-spreadsheet 功能太轻,连公式和数据校验都不完善;Luckysheet 曾经很火但维护节奏变慢;Handsontable 功能不错但商用授权是个门槛。综合下来,在这个"可嵌入 + 可锁定 + 可校验 + 有公式"的组合需求里,Univer 是最合适的。

2. 核心设计思路:把"只读/只填"做对

2.1 先理解 Univer 的单元格锁定模型

要做出"只能填某些格子"的效果,第一步不是写代码,而是理解表格系统里的权限模型。Excel 用户可能已经熟悉这套逻辑:每个单元格有个 locked(锁定)属性,同时工作表有一个 Protection(保护)总开关。只有总开关打开的时候,locked 为 true 的单元格才真正不可编辑;locked 为 false 的单元格即使在保护状态下也可以填写。Univer 作为兼容 Excel 体系的表格组件,本质上沿用了这个模型。

所以我的实现思路非常清晰:默认把所有单元格都设置成锁定状态,然后只把允许填写的区域设置成未锁定;最后开启工作表保护。这样表头、标题、公式列、固定说明全部天然不可改,只有留出来的"白色输入区"可以操作。

这里有个容易被忽略的细节:如果只把格子设成 locked 而不开启工作表保护,那么这个 locked 属性根本不会生效。我第一次调试时就是犯了这个错,样式配了半天,表格照样随便编辑,后来才反应过来保护开关没打开。

2.2 模板结构怎么设计

在动手写代码之前,我建议你先想清楚模板和填写数据的关系,这决定了整体结构。我见过两种常见设计:

一种是单表结构。一张表里既有固定内容和填写区,适合一次性填写、行数固定的小模板,比如报销单、申请单。这种结构简单直观,权限配置也方便:把整行整列的区域划开,固定区锁定、填写区开放就行。

另一种是双表结构。一个隐藏的 sheet 放模板配置、选项字典、辅助计算,另一个可见的 sheet 放用户填写的数据。适合需要批量收集、行数不固定的场景,比如项目周报、客户信息库。这种结构下,模板 sheet 完全锁定,数据 sheet 只有指定列开放编辑,数据回收后还能在后台把两个 sheet 关联起来做汇总。

我这次做的是报销单场景,用的是单表结构,原因很简单:每一行的填写结构完全一致,锁定区域就是表头和公式列,填写的只有中间几列,权限边界非常明确。

2.3 可填区域的可见性设计

权限做对了只是第一步,用户能不能一眼看出哪里能填,才是这个功能最终好不好用的关键。我见过一些产品,确实把单元格锁了,但可编辑区和锁定区长得一模一样,用户进来之后到处乱点,体验非常差。

我的建议是:用视觉把"可填"信号做得极其明显。锁定区域统一用浅灰色背景,可填写区域用白色或淡黄色背景,再给可填区域加一圈醒目的边框;有条件的还可以在表头下面用背景色区分"填写区"和"说明区"。这个看起来是小事,但能省掉大量使用培训成本。

另外要设计好占位提示。Univer 的单元格是可以写文本的,我习惯在模板里用浅色斜体文本写"请填写项目名称""请输入数字"之类的提示语,等用户真正输入时再覆盖掉。提交时后端要把"提示语"和"用户真实填写值"区分开,我的做法是提示语放在一个固定的辅助列里,或者在初始化模板时不写入真实数据区,只在注释或批注里提示。

2.4 数据收集链路设计

单元格锁定的最终目的是"收上来的数据是干净的"。所以我在设计时把整条链路想成三段:

第一段是模板生成。管理员在后台用 Univer 打开一个空白工作簿,进行表头设置、合并单元格、写公式、加下拉校验,然后保存。保存的其实是 Univer 的工作簿数据结构(包含 cellData、styles、protection 配置等),可以整体序列化成 JSON 存到后端。这样模板的版本管理也顺带解决了——每次保存就是一个新版本。

第二段是用户填写。前端从后端拉取模板 JSON,用 Univer 渲染出来,用户只能在未锁定的单元格里输入。输入过程中,数据校验会实时拦截明显错误,比如金额列填了非数字、必填项留空。

第三段是数据回收。用户点提交时,前端通过 Univer 的 API 把指定区域的值读出来,拼成结构化数据提交给后端。后端再做一次业务校验,比如金额合计是否正确、工号是否存在,校验通过后入库。

3. 实操过程与核心实现

3.1 最小工程初始化

我先说下我的搭建方式。项目用的是 Vite + TypeScript,依赖直接走 npm。以我锁定的 Univer 0.2.x 版本为例,核心需要这几个包:

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

如果版本更新后引入了 preset 包,可以按官方文档换成@univerjs/preset-sheets,减少手动配依赖的麻烦。页面里只需要放一个容器节点:

<div id="app" style="width: 100%; height: 600px;"></div>

然后初始化一个包含单个工作簿、单个工作表的 Univer 实例。下面这段代码是一个最小可用骨架:

import { Univer } from '@univerjs/core'; import { defaultTheme } from '@univerjs/design'; import { UniverSheetsPlugin } from '@univerjs/sheets'; import { UniverSheetsUIPlugin } from '@univerjs/sheets-ui'; import { UniverUIPlugin } from '@univerjs/ui'; const univer = new Univer({ theme: defaultTheme }); univer.registerPlugin(UniverSheetsPlugin); univer.registerPlugin(UniverUIPlugin, { container: 'app', }); univer.registerPlugin(UniverSheetsUIPlugin); const workbook = univer.createUniverSheet({ id: 'workbook-expense', name: '报销单模板', sheets: [ { id: 'sheet-expense', name: '报销单', rowCount: 30, columnCount: 8, }, ], });

这段代码本身没什么业务含义,但它是后面所有功能的地基。要注意的是,Univer 的 API 在不同版本间变化比较快,我建议在项目里把 Univer 版本号锁死,不要随手升级。

3.2 用数据定义模板

我习惯用工作簿数据结构来定义模板,因为这样模板可以序列化、存库、再恢复。数据结构的核心是cellData:外层 key 是行号,内层 key 是列号,值是单元格对象。比如给第一行写表头、给第二行写提示语:

const workbookData = { id: 'workbook-expense', name: '报销单模板', sheets: [ { id: 'sheet-expense', name: '报销单', rowCount: 30, columnCount: 8, cellData: { 0: { 0: { v: '费用项目' }, 1: { v: '金额(元)' }, 2: { v: '票据张数' }, 3: { v: '发生日期' }, 4: { v: '说明' }, 5: { v: '合计' }, }, 1: { 0: { v: '请选择费用类型' }, 1: { v: '请输入数字' }, // 其他列同理 }, }, }, ], }; univer.createUniverSheet(workbookData);

需要合并单元格的表头,也可以在初始化数据里直接声明mergeData;想给模板加固定列宽,可以配置columnData里的w(宽度)字段。这些细节目的只有一个:让模板打开就是一份"排好版的 Excel",用户不需要做任何格式调整。

这里我要强调一个经验:模板里的计算公式一定要交给 Univer 处理,而不是提前算好值写死。比如"合计"这一列,我用的是公式单元格,把当前行的金额自动加起来。因为公式单元格在保护状态下是不可编辑的,用户永远破坏不了它;而且当用户修改金额时,合计会自动重算,数据一致性由表格内核保证。

3.3 开启保护并锁定非填写区域

这一步是整个需求的核心。我把实现拆成两个动作:先给可填写单元格打上 unlocked 标记,再开启工作表保护。

在 Univer 的数据结构里,单元格样式可以通过styles映射表定义,其中locked字段控制锁定状态。我给两类单元格分别定义样式:锁定区用灰色底、未锁定区用白色底。具体实现是把locked字段加进单元格的样式引用里,然后通过动态修改样式或初始化数据来生效。

以初始化方式为例,我在styles里定义两套样式,然后在cellData里给对应单元格引用:

const styles = { inputCell: { bg: { rgb: '#ffffff' }, locked: false, }, readonlyCell: { bg: { rgb: '#f2f2f2' }, locked: true, }, }; // 在 cellData 里引用样式: // 0 行表头用 readonlyCell,1 到 20 行的填写列用 inputCell

配置好样式后,再在 sheet 数据里开启保护。不同版本字段名会漂移,但大体结构是:先在工作表数据里加上protection配置,把整表设为保护状态,同时允许用户选中锁定单元格(只选中、不编辑),禁止插入行列、删除行列、修改格式等危险操作:

protection: { options: { selectLockedCells: true, selectUnlockedCells: true, formatCells: false, formatColumns: false, formatRows: false, insertRows: false, deleteRows: false, }, ranges: [], }

selectLockedCells这个选项是我特别提醒要打开的地方。如果把它设成 false,用户连点击灰色格子都不行,很容易产生"表格坏了"的错觉。打开后,用户仍然能点选、查看内容,只是不能修改,体验上接近"可读但不可写"。

如果你的版本已经提供了 UI 层的"保护范围"入口,那更省事:在界面上选中要锁定的区域,右键设置保护即可。但我个人还是推荐用数据配置的方式,因为模板要存后端、要动态下发,把保护规则固化在模板 JSON 里才是可持续的做法。

3.4 读取填写数据并提交

模板渲染出来后,用户填的就是普通单元格数据。提交环节我推荐用 Univer 的 Facade 门面 API,它把底层命令封装成了接近直觉的接口,读取值非常方便:

import { FUniver } from '@univerjs/facade'; const fUniver = new FUniver(univer); const sheet = fUniver.getActiveSheet(); // 读取第 1 行到第 20 行、第 0 列到第 4 列的数据 const values = sheet.getRange(1, 0, 20, 5).getValues();

拿到的values是一个二维数组,可以直接序列化提交给后端:

const payload = { templateId: 'expense-001', data: values, }; await fetch('/api/expense/submit', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), });

我在这里踩过一个不大不小的坑:getValues 返回的单元格值里,可能有空字符串、null、undefined 多种空值形态。后端如果用强类型解析,会被空值形态差异坑到。我的做法是前端做一层归一化:空值统一转成 null,数字类型统一转成 number,日期统一转成 ISO 字符串,再提交。这样后端拿到的一定是干净数据。

3.5 用数据校验提升填写质量

单元格锁定解决的是"不能改哪里"的问题,数据校验解决的是"填的内容对不对"的问题。Univer 支持数据校验,配置下拉列表、数字范围、必填等规则。比如费用类型这一列,我给它加了一个下拉校验,用户只能从预置选项里选:

const expenseTypes = ['餐饮', '交通', '办公用品', '差旅', '其他']; sheet.getRange(1, 0, 19, 1).setDataValidation({ type: 'list', formula1: expenseTypes, allowBlank: false, });

金额列则可以配成数字校验,范围限制在 0 到 100000,防止有人填负数或者乱码。这里要提醒一句:数据校验是"体验层"的约束,不是安全边界。用户在界面上可能被拦住了,但如果是通过接口绕过前端直接提交,校验就失效了。所以后端一定要把同样的校验规则再实现一遍,前端校验只是减少无效请求,后端校验才是数据质量的最终防线。

3.6 多人协作场景的注意点

Univer 的定位是支持多人实时协作的,这在多人同时填写同一张表时很有价值,但也带来两个新问题。

第一个是锁粒度。如果是多人不同行填写同一张表,问题不大;但如果是多人同时往同一行甚至同一个单元格填,冲突就来了。Univer 底层有协作能力,但我这种业务不用实时同步,因为我需要的是"每个人各自填一份、最后回收汇总"。所以我实际做的是:每人下发一份模板副本,提交时带上唯一提交人标识,后端按人存储,互不影响。

第二个是模板和数据的隔离。协作场景下千万别让普通用户看到模板编辑界面,否则你保护规则做得再好,也防不住拿着管理员权限的人改模板。我的做法是路由层面区分角色:管理员进编辑页,普通用户进填写页。就算同一个工作簿数据,前端也可以根据用户角色决定是否开启保护。

4. 常见问题与排查实录

4.1 保护区域还能被编辑

这是最常遇到的问题,我第一次也栽在这里。排查思路从三个方向走:

先看工作表保护是否真正开启。很多人只在单元格样式里设置了 locked,忘了开 protection,结果自然是摆设。再看样式引用的locked字段有没有写反——我见过把可填写区设成 locked: true 的配置,整个表全禁了。最后看是不是缓存问题:模板 JSON 被浏览器缓存了,用户打开的还是旧数据。这种问题刷新无用,要清缓存或者换个标识让前端强制拉新模板。

4.2 下拉校验没有生效

我遇到过设置了下拉,但用户点单元格没有出现下拉箭头的情况。多半是选中范围写错了——校验配置到 A1:A10,用户填的是 B 列,自然不生效。还有一种情况是数据校验 API 在渲染完表格之后才调用,而 Univer 的 UI 没有来得及刷新。我的解决办法是:把校验规则也写进工作簿数据,随模板一起初始化,而不是等渲染完再动态设置。这样既稳定又避免了时序问题。

4.3 复制粘贴和拖拽把锁定状态带乱

这个坑很隐蔽。用户在一个可填写单元格里复制内容,粘贴到锁定单元格时,有些版本保护生效没拦住,直接把锁定单元格的样式也带过去了。更麻烦的是拖拽填充:用户按住填写区右下角往下拖,会把"未锁定"的样式传播到旁边本应锁定的单元格。

我的处理方案是双保险:一是在开启保护的基础上,把"编辑对象""修改样式"等权限选项全部关掉;二是写完模板后做一次全表保护状态巡检——用后端脚本检查所有单元格的 locked 属性是否符合预期,防止样式被用户在操作中污染。巡检脚本放在保存模板的后台接口里,每次保存都跑一遍。

4.4 大数据量模板卡顿

表格渲染用的是 Canvas,性能比 DOM 表格好很多,但不代表不会卡。我把模板行数加到几万行以后,滚动和编辑明显变慢。后来定位到两个原因:一是整个工作表塞了大量合并单元格和自定义样式,样式对象多导致渲染负担重;二是我不小心给整列都设置了背景色,非填写区域的样式覆盖内存占用非常大。

解决办法很直接:行数和列数控制在实际业务需要范围内,比如报销单 30 行就够了,没必要配置成 100 万行;样式尽量共用,不要给每个单元格配一个独立样式对象;再用冻结窗格把表头固定住,给用户一个清晰的浏览体验。

4.5 版本升级后接口变了

Univer 迭代快,我从 0.1.x 升到 0.2.x 的时候,很多命令和数据结构字段都变了,编译直接报错。这是我后来决定锁版本的原因。如果你也要升级,我建议先看官方 changelog,把工作簿数据结构、插件注册方式、Facade API 这三块的变化列出来,再在测试环境用真实模板 JSON 跑一轮回归。不要边写业务边升级,那是给自己挖坑。

4.6 问题排查速查表

现象可能原因处理办法
锁定区域仍可编辑保护开关未开启检查 protection 配置,确保整表保护打开
锁定区域仍可编辑locked 样式配置相反检查 styles 里 locked 字段方向
锁定区域仍可编辑浏览器加载旧模板清缓存、增加模板版本标识强制刷新
下拉不出现数据校验范围写错将校验规则写入模板数据,随初始化生效
粘贴/拖拽破坏锁定保护选项没限制修改关闭样式修改权限,增加后端巡检
表格卡顿行列数过大、样式冗余缩小行列范围,合并公共样式,冻结窗格
提交数据有空值形态混乱前端未归一化将空值统一为 null、数字转 number 再提交
接口报错版本升级导致字段变化锁版本,升级前看 changelog 并做回归

5. 这件事还能怎么玩

5.1 模板版本化

模板 JSON 能存后端,就天然支持版本管理。我的做法是每次管理员保存模板,都记录一版快照,带上版本号、修改人、修改时间。用户填写时要么用最新版,要么由管理员指定某历史版本。这样即使有人把模板改坏了,也能迅速回滚,不会影响正在进行的数据收集任务。

5.2 按角色分配可填区域

同一张模板,不同人可以有不同的可填范围。比如项目负责人能填"风险"列,普通成员只能填"本周进展"列。实现上就是把保护规则从"全表一份"细化成"按用户角色动态生成"。前端根据当前登录人的角色,在初始化模板时动态调整哪些单元格解锁。Univer 的底层能力完全支撑这种动态配置,业务上只需要维护好角色和区域的映射关系。

5.3 对接审批与归档

数据收上来之后,可以在后端把提交记录串成审批流,然后利用 Univer 的导出能力生成 xlsx 归档文件。我目前的做法是:提交 -> 系统校验 -> 入库 -> 导出归档 xlsx -> 上传到文件服务,整个过程全自动。对财务、人事这类经常需要提供原始凭证和审计文件的场景,这个链路能省掉大量人工整理时间。

最后再分享一个我做这个功能最大的体会:不要把 Univer 当普通表格组件用,要把它当业务系统的基础设施来看待。它的价值不止是"画表格"这么简单,而是把 Excel 那套成熟的单元格模型开放给了前端,让业务规则可以精确落到每一个格子上。用好了,你完全可以在这个底座上叠加出比传统表单更灵活、比丢文件更规范的数据收集方案。如果你正在做类似的功能,建议先在小范围把模板设计、保护规则、数据回收这条链路跑通,再逐步加权限、加校验、加审批,稳扎稳打比一开始追求大而全要靠谱得多。

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

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

立即咨询