指定區(qū)域可編輯的完整指南)
“想用在線表格做一個報名表或者工單登記表業(yè)務同學打開網(wǎng)頁就能填但只能改我指定的那幾格標題、說明還有那些公式列一概不能碰”——這是我最近被問到次數(shù)最多的一個需求。字面聽上去不復雜真正落到代碼里才發(fā)現(xiàn)彎彎繞繞不少。如果你正在用Univer搞這類“用戶定義表格 指定單元格填寫、其他單元格鎖定”的在線表格場景或者剛接觸 Univer 在評估它能不能扛起這個業(yè)務這篇就是給你準備的實操筆記。我會從項目選型講到權限模型再給可直接抄的配置和代碼最后整理幾個我實際踩過的坑。1. Univer 是什么項目定位與核心技術拆解1.1 開箱即用的在線表格內核Univer 是一套基于 TypeScript 構建的開源辦公套件方案主打在線創(chuàng)建、編輯和預覽電子表格、文檔與幻燈片。如果你聽過 Luckysheet那可以把它理解為 Luckysheet 團隊在架構上推翻重做后的下一代產物渲染層換成了 Canvas 繪制核心與 UI 徹底解耦插件化程度更高還內置了一套公式引擎和命令系統(tǒng)。從業(yè)務接入角度Univer 最讓人舒服的一點是它不是給你一個“靜態(tài)表格組件”而是給你一個完整的、可編程的表格運行時。你可以在里面定義工作簿、工作表、單元格數(shù)據(jù)、公式、樣式、數(shù)據(jù)校驗也可以通過命令服務去執(zhí)行“設置單元格值”“合并單元格”“開啟工作表保護”這些操作。換句話說你不需要像過去那樣在一堆 div 和 table 標簽上模擬表格行為而是直接操作一個真實存在的電子表格實例。Univer 的模塊劃分大概是這樣univerjs/core核心數(shù)據(jù)模型工作簿、工作表、單元格、區(qū)域、樣式、命令框架都在這層。univerjs/sheets表格業(yè)務邏輯包括單元格編輯、選區(qū)、公式計算、篩選、排序等基礎能力。univerjs/sheets-ui表格 UI 層負責渲染工具欄、編輯欄、右鍵菜單、彈窗。univerjs/ui通用 UI 基礎設施讓 Univer 可以嵌入 React、Vue 或原生 JS 項目。univerjs/engine-formula公式引擎用來處理跨表、跨工作簿的公式計算。univerjs/engine-renderCanvas 渲染引擎負責把表格畫到頁面上。這個分層直接決定了它的擴展性。比如你只想用表格編輯能力不想顯示官方那一整條工具欄那完全可以不注冊 UI 插件自己寫一套編輯入口反過來如果你需要標準 Excel 體驗把 sheets-ui 注冊上就基本齊了。正因為這種靈活度Univer 特別適合做“半定制化”的業(yè)務表格而不是只能全盤照搬的標準 Excel。1.2 為什么選 Univer 做“用戶填表”場景做“用戶填寫指定單元格”這件事傳統(tǒng)方案一般有三條路直接發(fā) Excel 模板讓人填了再回收、用傳統(tǒng)前端表格組件仿一個填表頁、或者直接在自己系統(tǒng)里做表單引擎。這三條路各有各的別扭Excel 模板回收版本混亂、格式被改、收集匯總全靠人肉體驗很差。前端表格組件仿填表頁表格行為很難做到位。用戶想要拖動填充、下拉選擇、公式聯(lián)動時基本都要自己造輪子。表單引擎雖然能限制輸入項但表達不了表格布局。比如一行一個項目的工時填報或者橫豎軸交叉的排班表用表單控件排出來非常痛苦。Univer 剛好卡在中間它有原生表格的交互能力又允許你用編程方式控制哪些單元格可編輯、哪些被鎖定。管理員先在頁面上把模板搭好鎖定不需要用戶碰的區(qū)域再把鏈接發(fā)給用戶用戶在網(wǎng)頁里只能按預定位置填寫。整個流程在線上閉環(huán)數(shù)據(jù)直接回傳后端既避免了 Excel 文件滿天飛又保留了表格天然的布局表達能力。更關鍵的是Univer 的保護機制不是“只能設置整表只讀”這種一刀切它支持把工作表的“保護”和“單元格的鎖定屬性”拆開組合配合非常細的權限范圍能夠準確實現(xiàn)“某些區(qū)域可以編輯、其他區(qū)域不能改”的需求。這就是這篇實操里最核心的切入點。2. 需求拆解讓用戶填寫指定單元格其余鎖定2.1 “用戶定義表格”的業(yè)務本質先把“用戶定義表格”這個說法拆開。用戶這個詞在不同場景里指代不一樣。在多數(shù)業(yè)務系統(tǒng)里設計表格模板的是管理員或財務最終填寫數(shù)據(jù)的是普通員工或外部客戶。所以“用戶定義表格”實際上包含兩層含義模板定義權誰來創(chuàng)建表格結構、設置標題、公式、校驗規(guī)則、鎖定規(guī)則。數(shù)據(jù)填寫權誰能在特定區(qū)域內填入內容。這篇文章要解決的核心是第二層但實現(xiàn)第二層之前必須先想清楚第一層。因為模板的定義過程往往也需要在 Univer 里面完成如果管理員自己都分不清哪些單元格是鎖定用的、哪些是放開用的后面所有規(guī)則都是空中樓閣。一個典型的業(yè)務例子是培訓報名表A1:D1 是合并標題“2025年第三期安全培訓報名表”。第二行是列名姓名、部門、郵箱、是否住宿。管理員不希望用戶改標題和列名甚至不希望用戶能選中這些單元格用戶只需要從第三行往下填寫自己的信息如果郵箱格式錯了表格應給出提示。這個例子里的“可編輯區(qū)域”就是一個從第三行到表格末尾的數(shù)據(jù)區(qū)域。用戶在這個區(qū)域里輸入內容其他區(qū)域要么鎖定、要么只讀。管理員創(chuàng)建模板時也應該把這個規(guī)則體現(xiàn)在配置里而不是等表格上線后再去臨時設置。2.2 權限模型與可編輯范圍控制的關鍵點Univer 控制可編輯性的機制本質上沿用了 Excel 那套經典的“工作表保護 單元格鎖定”模型。先記住一個關鍵結論單元格默認的鎖定狀態(tài)并不等于用戶不可編輯。只有當工作表開啟了保護protection之后鎖定屬性才會生效。這個關系可以類比成小區(qū)門禁每個房間有門鎖單元格 locked 狀態(tài)但只有保安啟動門禁系統(tǒng)工作表保護這些門鎖才真正起作用。如果你只給每個房間換了鎖卻讓保安放假那誰都能推門進去。在 Univer 的配置模型里工作表保護對象至少包含這幾個關鍵字段sheet布爾值表示這張工作表是否啟用保護。lockCells布爾值表示當前工作表是否鎖定所有單元格。ranges數(shù)組用來聲明保護范圍內的例外區(qū)域每個區(qū)域可以單獨設置是否允許鎖定。當lockCells: true且protection.sheet: true時整張表默認不可編輯只有ranges里明確列為 unlock 的區(qū)域可以編輯。反過來如果lockCells: false整張表默認可編輯ranges里的區(qū)域可以被單獨鎖死。大多數(shù)“用戶填表”場景用的是前者先鎖全表再把填寫區(qū)域放出來。還有兩個容易被忽略的配置項allowSelectingLockedCells和allowSelectingUnlockedCells。前者控制用戶能不能點選鎖定區(qū)域后者控制用戶能不能點選可編輯區(qū)域。如果業(yè)務上要求“用戶連標題都選不中”就把allowSelectingLockedCells設為 false如果允許用戶點選已填寫的內容只是不能修改那就保持為 true。我這邊的經驗是填表業(yè)務里通常把兩個都放開因為用戶選中有助于看清他填過什么只要不能編輯就可以了。另外要注意Univer 的保護模型是工作表級別的不是工作簿級別。如果你想整個工作簿都進入“填表模式”需要遍歷里面每一張工作表分別設置保護。如果有多個 Sheet且用戶應該只能看到其中一張?zhí)顖蟊砟歉鼘嵱玫淖龇ㄊ侵苯与[藏其他工作表只保留目標表。3. 實操在 Univer 中實現(xiàn)“可指定區(qū)域填寫”3.1 環(huán)境準備與最小示例先搭一個最小可運行的 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);這一步跑起來以后頁面右上角會出現(xiàn) Univer 自己的工具欄。我建議開發(fā)階段不要急著隱藏工具欄因為“保護工作表”這個功能在工具欄里可以直接點方便你驗證效果。生產環(huán)境再根據(jù)業(yè)務隱藏不必要按鈕。然后在項目里創(chuàng)建一張工作表數(shù)據(jù)。Univer 創(chuàng)建表格時可以傳一個類似“工作表配置快照”的對象里面包含單元格數(shù)據(jù)、樣式、合并信息、行高列寬還有我們關心的保護配置。下面的示例是完整模板結構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 行的前四列放出一個可編輯區(qū)域。用戶在網(wǎng)頁上打開后表頭區(qū)域內容灰色、選中但改不了第 3 行及以下的白色區(qū)域內用戶可以像操作 Excel 一樣輸入姓名、部門、郵箱和住宿信息。3.2 定義可填寫區(qū)域模板配置方式上一步的 protection 配置里ranges就是指“放給用戶編輯的區(qū)域”。這個數(shù)組可以包含多個不連續(xù)的區(qū)域比如一張表上既有“基本信息區(qū)”又有“家庭成員區(qū)”那就寫兩個 range 對象。注意startRow和endRow、startColumn、endColumn都是從 0 開始計數(shù)的表格第 1 行對應 startRow 0第 1 列對應 startColumn 0。這個計數(shù)方式特別容易踩坑我第一次寫就把第三行寫成了 startRow 2 還是 3 糾結了半天。單元格樣式里的locked是給單元格本身打標用的。如果你在創(chuàng)建模板時想明確某個單元格“永遠不能被編輯”除了在 protection 的 ranges 里不放這個區(qū)域還可以同時在s.locked上做標記。但需要說明locked 只是標簽保護范圍才是最終執(zhí)行依據(jù)。開啟保護后Univer 執(zhí)行編輯命令時會去檢查“當前選中區(qū)域是否在 protection 的允許編輯范圍內”。所以模板正確性最終看 protection.ranges單元格的 locked 屬性用來配合 UI 展示比如給鎖定區(qū)域加淺灰背景讓用戶一眼看出哪里不能填。3.3 運行時切換編輯權限命令方式模板配置是“靜態(tài)初始化”的做法適合表格結構在代碼里寫死。但真實業(yè)務里管理員很可能要在界面上臨時修改可編輯范圍這就需要用命令動態(tài)調整保護配置。Univer 的命令服務CommandService是運行時改變表格狀態(tài)的唯一正規(guī)入口。示例代碼如下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 }, ], }, });這里有兩個 idunitId是工作簿的 idsubUnitId是工作表的 id。在我這個例子里兩者都用了同一個字符串。如果你的業(yè)務里有兩張 sheet那 subUnitId 就分別指向對應 sheet。很多初學時困惑的“為什么執(zhí)行命令沒反應”八成是這兩個 id 沒對上。調試時可以先打印工作簿和工作表的 idconst workbook univer.getActiveWorkbook(); const worksheet workbook.getActiveSheet(); console.log(workbook.getId(), worksheet.getId());執(zhí)行完這個命令后表格的編輯權限會立即變化不需要刷新頁面。這種動態(tài)控制很適合做“審批流”管理員編輯模板時保護未開啟審核通過后調用命令開啟保護業(yè)務用戶拿到鏈接就進入了填寫模式。3.4 前端純攔截兜底方案保護機制原理上是攔截了 Univer 內部的編輯命令但并不是所有交互都能被 protection 覆蓋。比如某些版本里你仍然可以通過填充柄向下拖拽一個鎖定區(qū)域的值或者復制鎖定區(qū)域粘貼到可編輯區(qū)域。遇到這種情況光靠 protection 不夠還得在前端事件層做兜底。Univer 提供命令執(zhí)行監(jiān)聽我們可以攔下不必要的操作univer.getCommandService().onCommandExecuted((command) { if (command.id sheet.command.set-range-values) { // 檢查 command.params 里的 range 是否落在可編輯區(qū)域內 // 如果不在保護范圍內就攔截或者回滾 } });更簡單的做法是在編輯器外層加一層業(yè)務校驗拿到用戶提交數(shù)據(jù)后在后端再次校驗“提交的字段是否都位于允許范圍內”。前端保護是為了體驗后端校驗才是底線。這個原則放在任何表格權限場景都適用。4. 進階協(xié)同填寫與后端保存的完整方案4.1 多用戶同時填寫時保護區(qū)域怎么保證如果你只是把一張帶保護的工作表發(fā)給用戶每個用戶獨立打開、獨立填寫那權限控制很清晰。但現(xiàn)實里經常出現(xiàn)幾十個人同時打開同一張表各自往自己那一行填數(shù)據(jù)。Univer 本身支持協(xié)同編輯底層可以用 WebSocket、Yjs 等同步但協(xié)同模式下保護邏輯的復雜度會上升。首要原則是不要把后端權限校驗寄托在前端保護上。前端保護只是 UI 層面的限制協(xié)同服務收到操作指令后必須自己校驗這個 range 是否允許寫入。否則一個懂點前端的人可以繞過界面直接調協(xié)同同步接口把鎖定區(qū)域的數(shù)據(jù)改掉。具體操作層面我建議把“保護配置”提升為后端的一張配置表這張表里存了工作簿的 unitId、sheet 的 subUnitId、允許編輯的 ranges 列表。用戶發(fā)起編輯時協(xié)同服務先查配置表校驗操作范圍再決定是否放行。如果業(yè)務里用到了 Univer 官方協(xié)同方案可以基于其命令廣播機制在命令進入同步管道之前加一個鑒權中間層。4.2 把填寫結果持久化到后端填表業(yè)務最終要落庫。Univer 里讀取用戶填寫內容有幾種做法用戶填完后前端統(tǒng)一從表格實例中取出整個數(shù)據(jù)區(qū)。監(jiān)聽單元格變更事件實時增量提交。第一種做法適合“填完點提交”的流程。代碼大概這樣const worksheet univer.getActiveWorkbook().getActiveSheet(); // 讀固定區(qū)域的數(shù)據(jù) 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 到后端接口。這里的重點是讀取數(shù)據(jù)時不要讀全表只讀你允許填寫的區(qū)域既減少不必要的傳輸也天然規(guī)避了越權數(shù)據(jù)被帶上來的風險。第二種做法適合表格長期打開、自動保存的場景。Univer 的命令服務有對應事件univer.getCommandService().onCommandExecuted((command) { if (command.id sheet.command.set-cell-value) { // 把 command.params 里的 values 增量提交 } });增量提交要做防抖不然用戶連續(xù)輸入十幾個字符會打出十幾條請求。我習慣把變更先緩存到一個 Map 里用 500ms 的定時器統(tǒng)一上報。4.3 配合表單校驗與數(shù)據(jù)聯(lián)動“能編輯”和“能填對”是兩回事。用戶雖然只能寫指定區(qū)域但寫出來的內容可能格式完全不對。Univer 在填表場景下最好開啟數(shù)據(jù)校驗能力。比如郵箱列可以在模板配置里給單元格加上校驗規(guī)則或者使用公式做判斷。Univer 支持在初始化時給單元格指定 validator在填表業(yè)務中更實用的做法是監(jiān)聽值變更在 UI 上實時提醒。這里有一個經驗校驗規(guī)則不要寫在保護配置里也不要散落在模板各處最好集中在一個數(shù)據(jù)字典結構里帶進模板這樣后端校驗和前端提示共用同一份規(guī)則避免兩邊不一致。舉個例子郵箱列的校驗規(guī)則可以定義為{ type: regex, pattern: ^[\\w.-][\\w-](\\.[\\w-])$, message: 郵箱格式不正確, }用戶填完不合法提交按鈕置灰只有全部合法才能提交。這提升了表格的可用性也讓后臺少收很多臟數(shù)據(jù)。5. 常見問題與避坑指南5.1 保護開啟后鎖定區(qū)域仍能編輯這是我在社區(qū)里看到最多的問題自己第一次也遇到。排查順序如下確認 protection 的sheet是否真的為 true。很多人只設置了lockCells沒有把sheet打開保護等于沒啟用。確認lockCells是否為 true。如果 lockCells 為 falseranges 之外的區(qū)域默認可編輯保護范圍的含義反過來了。確認 ranges 的lock字段??删庉媴^(qū)域要用lock: false顯式標記。有些版本字段名是locked拿到的示例代碼里寫的是lock就照抄結果毫無反應。確認執(zhí)行的是重新設置整套 protection而不是增量 patch。Univer 命令執(zhí)行時通常會整體替換 protection 對象所以每次更新都要把完整的 ranges 放進去。5.2 公式計算、填充柄繞過保護保護只能攔編輯命令攔不住用戶把可編輯區(qū)域的公式向下填充到鎖定區(qū)域。如果想徹底避免這種問題有兩個思路在鎖定區(qū)域不上公式改由后端統(tǒng)一計算。在事件層攔截填充操作只允許在非鎖定區(qū)域范圍內執(zhí)行。第二種思路實現(xiàn)起來要監(jiān)聽比較底層的命令比較麻煩。我的建議是填表場景能不用公式就不用公式。表格的計算能力讓管理員在后臺設計模板時用最終提交給用戶的填表視圖盡量只展示普通文本和數(shù)字把公式計算挪到保存后的結果頁。5.3 Excel 導入與保護兼容性問題Univer 支持導入 xlsx但導入文件的保護配置不一定能完整還原。Excel 里“允許用戶編輯區(qū)域”是通過范圍安全性設置的Univer 從 xlsx 里解析時可能會丟失或者轉換偏差。如果業(yè)務要求管理員先上傳 Excel 模板再由系統(tǒng)啟用填表模式我的建議是上傳后不要依賴原文件的保護屬性而是按業(yè)務規(guī)則重新生成 protection ranges。怎么做呢上傳后先解析 Excel 的單元格結構然后通過規(guī)則匹配出可編輯區(qū)域。比如約定“所有帶黃色背景的單元格為可編輯區(qū)域”解析時讀單元格背景色把這些坐標轉成 ranges。這樣做的好處是模板設計者不需要懂 Univer API只要在 Excel 里涂色就行。5.4 大表格初始化性能填表模板一般不會太大但如果管理員從 Excel 導入了幾千行數(shù)據(jù)Univer 初始化時全量渲染會卡頓。優(yōu)化手段有幾個減少初始 cellData 里所有單元格都賦空對象的情況Univer 對稀疏數(shù)據(jù)渲染更友好。隱藏不必要的行列不要讓用戶看到空白區(qū)域。條件格式和校驗規(guī)則不要鋪滿整張表只設置在真正的數(shù)據(jù)區(qū)域。如果可編輯區(qū)域很單調優(yōu)先用 range 規(guī)則代替逐格樣式盡量減少單元格級對象數(shù)量。6. 一套可直接使用的“可填寫表格”配置參考6.1 模板配置速查最后給一份完整的、可以改改就用的配置。我用“工時統(tǒng)計”舉例管理員每月發(fā)一張表給組員填寫組員只能填“項目名稱、工時、說明”三列其他列鎖定。工作表規(guī)劃第 1 行合并標題。第 2 行列名項目編號、項目名稱、工時、說明。第 3 行到第 20 行填寫區(qū)。項目編號列鎖定內容由系統(tǒng)寫入項目名稱、工時、說明列可編輯。配置如下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, }, ], }, };用戶打開后項目編號列由系統(tǒng)預先填好用戶只能填項目名稱、工時、說明三列。這樣收集上來的數(shù)據(jù)非常規(guī)整后端解析也方便。6.2 事件聯(lián)動填寫完成后的處理一個比較好用的小技巧是監(jiān)聽單元格變更之后把變更單元格標成其他背景色這樣用戶一眼就能看出自己填了哪些格子管理員也能快速判斷哪些數(shù)據(jù)是新增的。核心代碼就幾行univer.getCommandService().onCommandExecuted((command) { if (command.id sheet.command.set-cell-value) { const { unitId, subUnitId, values } command.params; // 遍歷 values把對應單元格背景色置為淺綠或淺黃 } });要提醒一句這個監(jiān)聽事件非常頻繁做樣式更新時最好合并批處理避免每輸入一個字符就重繪一次。我一般把待更新格子攢到一個數(shù)組里等事件循環(huán)空閑時統(tǒng)一應用。這個“可填寫區(qū)域控制”的功能真正的關鍵不在 API 調用而是把權限模型想清楚。前端保護做得再好也只是給用戶一個順暢的操作邊界后端必須持有同一份規(guī)則做最終校驗。我在實際項目里被坑最慘的一次就是前端保護全做好了協(xié)同接口漏了校驗結果用戶直接繞過界面改掉了鎖定的公式列。后來我把 ranges 配置抽成公共模塊前后端共用問題才徹底消失。你動手做的時候建議第一步就先設計好這份公共配置再碰 Univer 的代碼后面會省掉非常多的返工。