Univer实战:模板表格指定单元格可编辑与工作表保护完整指南
前阵子有个朋友找我说他们业务系统里要做一张“只能填一部分格子的在线表格”——表头、指标、公式都是固定的用户只需要在某几列录入数据其余单元格死活不能动。我一听这不就是典型的数据填报场景嘛。关键是他需求里明确点了一个名字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 / 初始化单元格数据权限控制指定区域可编辑其余锁定工作表保护 解锁范围输入约束限制值域、格式、必填数据校验 DataValidationUI 反馈让用户一眼知道哪里能填样式区隔、占位提示、只读视觉每一层都不是万能的。模板定义只负责“长什么样”权限控制负责“能不能改”数据校验负责“改得对不对”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 idapp stylewidth: 100vw; height: 100vh;/divimport { 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 列给用户填备注。其他任何区域都不可编辑。完整操作流程如下构建模板快照在 A 列和 B 列写入固定内容对第 1 行写表头固定合并单元格、设置底色加载模板到 Univer执行工作表保护允许编辑范围设为 C2:D9给 C2:C9 添加下拉校验值为“正常、异常、待复检”给 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 组件一个格子一个格子拼 UIUniver 的好处是模板天然就是表格设计成本极低。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而是利用它的数据模型、命令系统、渲染扩展点把它嵌到你的业务流程里。先跑通“模板 保护 校验”这条最小链路往后做协同、做提交、做审批就都有基础了。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →