模板发放之后,通常有两类相反的诉求。一类是保护:表格、名单、条款已经定稿,不希望填写人误改或误删,但仍要保留控件结构,方便后续汇总。另一类是清理:文档已经定稿归档,内容控件不再需要,标签和外框反而干扰阅读,这时要把控件外壳去掉、只留内容。
锁定通过 SDTProperties.LockSettings 完成,移除则是把控件从父对象中取出、再把它的子内容按原顺序放回。两个操作方向相反,处理时都需要区分行内控件与块级控件。
本文用 Spire.Doc for JavaScript 演示这两种处理,全部在浏览器中完成,文档不离开用户设备。
本文介绍两个功能点:
两个功能点都围绕 StructureDocumentTag(块级)与 StructureDocumentTagInline(行内)展开:
| 对象类型 | DocumentObjectType 取值 |
移除时的父对象 |
|---|---|---|
| 行内内容控件 | StructureDocumentTagInline |
所在段落 |
| 块级内容控件 | StructureDocumentTag |
节正文或单元格 |
有关安装和项目配置,请参考 React 项目中集成 Spire.Doc for JavaScript。下面的示例假定 Spire.Doc 已安装、WebAssembly 模块已初始化。
锁定内容控件
锁定用来约束填写人在 Word 中的操作。LockSettings 是一个枚举,不同取值保护的对象不同:有的只保护控件内容,有的只保护控件本身,有的两者都保护。
可用的锁定模式如下:
LockSettingsType 取值 |
写入的 OOXML | 保护效果 |
|---|---|---|
UnLocked |
不写入 w:lock |
不锁定,可编辑也可删除 |
ContentLocked |
w:lock w:val="contentLocked" |
内容不可编辑,控件本身可删除 |
SDTLocked |
w:lock w:val="sdtLocked" |
控件不可删除,内容仍可编辑 |
SDTContentLocked |
w:lock w:val="sdtContentLocked" |
内容不可编辑,控件也不可删除 |
具体操作步骤如下:
- 获取 Spire.Doc 的 WASM 模块,确认已就绪。
- 载入字体,创建文档并添加一节,用
AppendHTML()写入一张成员名单表格。 - 再添加一节,写入标题与说明文字,创建块级
StructureDocumentTag控件。 - 将表格克隆到控件的
SDTContent,设置LockSettings为ContentLocked,删除用于生成表格的临时节。 - 使用
SaveToFile()保存结果文件,从 VFS 读出并触发浏览器下载。
锁定内容控件的代码如下:
const FONT_FOLDER = '/Library/Fonts/';
const loadFontToVfs = async (fileName) => {
const url = `${process.env.PUBLIC_URL || ''}/static/font/${fileName}`;
const res = await fetch(url);
if (!res.ok) {
throw new Error(`字体加载失败:${url} 返回 HTTP ${res.status}`);
}
const bytes = new Uint8Array(await res.arrayBuffer());
const tag = bytes.length >= 4 ? String.fromCharCode(bytes[0], bytes[1], bytes[2], bytes[3]) : '';
const isFont = tag === 'ttcf' || tag === 'true' || tag === 'OTTO' || tag === 'wOFF' || tag === 'wOF2'
|| (bytes.length >= 4 && bytes[0] === 0x00 && bytes[1] === 0x01 && bytes[2] === 0x00 && bytes[3] === 0x00);
if (!isFont) {
throw new Error(`字体文件无效:${url} 返回的不是字体(常见于 404 或单页应用回退成 index.html)`);
}
const FS = window.dotnetRuntime.Module.FS;
FS.mkdirTree(FONT_FOLDER);
FS.writeFile(FONT_FOLDER + fileName, bytes, { flags: 'w+' });
};
function App() {
const lockContentControl = async () => {
// 等 WASM 运行时与 Spire.Doc 加载完成
if (window.spireReady) await window.spireReady;
const wasmModule = window.wasmModule?.spiredoc;
if (!wasmModule) {
alert('Spire.Doc is not ready yet');
return;
}
// 载入字体(AppendHTML 依赖字体做排版,至少要有一个有效字体写入 VFS)
const fontErrors = [];
let loadedFonts = 0;
for (const fileName of ['msyh.ttc', 'msyhbd.ttc']) {
try {
await loadFontToVfs(fileName);
loadedFonts += 1;
} catch (e) {
fontErrors.push(e.message || String(e));
}
}
if (loadedFonts === 0) {
alert(`字体加载失败,AppendHTML 将无法排版:\n${fontErrors.join('\n')}`);
return;
}
// 创建文档,并在第一节生成表格
const doc = new wasmModule.Document();
const tableSection = doc.AddSection();
const tableParagraph = tableSection.AddParagraph();
tableParagraph.AppendHTML(
'<table border="1" cellspacing="0" cellpadding="4" style="border-collapse:collapse;width:100%">'
+ '<tr><th> 编号</th><th>姓名</th><th>岗位 </th></tr>'
+ '<tr><td>01</td><td>吴敏</td><td>采购经理</td></tr>'
+ '<tr><td>02</td><td>赵启</td><td>技术总监</td></tr>'
+ '</table>');
// 新建一节,写入标题与说明
const section = doc.AddSection();
const titleParagraph = section.AddParagraph();
const titleRange = titleParagraph.AppendText('项目组成员名单');
titleRange.CharacterFormat.FontName = '微软雅黑';
titleRange.CharacterFormat.FontSize = 19;
titleRange.CharacterFormat.Bold = true;
titleRange.CharacterFormat.TextColor = wasmModule.Color.FromArgb(255, 15, 110, 90);
titleParagraph.Format.AfterSpacing = 14;
const hintParagraph = section.AddParagraph();
const hintRange = hintParagraph.AppendText('下表已锁定,名单内容不可直接修改。');
hintRange.CharacterFormat.FontName = '微软雅黑';
hintRange.CharacterFormat.FontSize = 9;
hintRange.CharacterFormat.TextColor = wasmModule.Color.FromArgb(255, 89, 89, 89);
hintParagraph.Format.AfterSpacing = 8;
// 创建块级内容控件
const sdt = new wasmModule.StructureDocumentTag(doc);
section.Body.ChildObjects.Add(sdt);
sdt.SDTProperties.SDTType = wasmModule.SdtType.RichText;
sdt.SDTProperties.Alias = '成员名单';
sdt.SDTProperties.Tag = 'member-list';
sdt.SDTProperties.Appearance = wasmModule.SdtAppearance.Tags;
// 把第一节中的表格克隆到控件内容中
for (let i = 0; i < tableSection.Body.ChildObjects.Count; i++) {
const obj = tableSection.Body.ChildObjects.get_Item(i);
if (obj.DocumentObjectType === wasmModule.DocumentObjectType.Table) {
sdt.SDTContent.ChildObjects.Add(obj.Clone());
}
}
// 锁定控件内容
sdt.SDTProperties.LockSettings = wasmModule.LockSettingsType.ContentLocked;
// 删除生成表格的临时节
doc.Sections.Remove(tableSection);
// 保存结果文件
const outputFileName = 'LockContentControlContent.docx';
doc.SaveToFile({ fileName: outputFileName, fileFormat: wasmModule.FileFormat.Docx2013 });
doc.Dispose();
// 从 VFS 读取生成的文件,触发下载
const fileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
const blob = new Blob([fileArray], { type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = outputFileName;
a.click();
URL.revokeObjectURL(url);
};
return (
<div style={{ textAlign: 'center', height: '300px' }}>
<h1>锁定内容控件</h1>
<button onClick={lockContentControl}>生成文档</button>
</div>
);
}
export default App;
锁定后的成员名单如下,块级控件的标签出现在表格内容的起止位置:

四种锁定模式的区别
四种模式可以按“保护内容”和“保护控件”两个维度来理解:
UnLocked是默认状态,Word 不写入任何锁定标记,内容与控件都可操作。ContentLocked只保护内容:填写人不能修改或删除控件里的文字、表格,但仍可以选中控件并将其整体删除。SDTLocked只保护控件:填写人可以修改内容,但不能删除控件本身,适合需要保留字段结构、允许改值的场景。SDTContentLocked两者都保护:内容和控件都不能删除,适合定稿后仅供阅读的条款、名单。
选择哪种模式,取决于该字段在流程中的角色。需要填写但结构固定的字段用 SDTLocked;已经定稿、只供查阅的段落用 SDTContentLocked;只读但允许整体替换的附件位用 ContentLocked。
移除内容控件并保留内容
归档文档时,内容控件的标签和外框会成为阅读干扰;把文档交给不熟悉控件的同事,对方也可能误以为需要填写。移除控件有两种含义:一种是连内容一起删掉,另一种是只去掉控件外壳、保留里面的文字与表格。后者更常用,也更需要小心处理顺序。
行内控件与块级控件的父对象不同:行内控件位于段落的 ChildObjects 中,块级控件位于节正文的 ChildObjects 中。示例从 public/data 读取 RemoveContentControls.docx,运行时请把该文件放入项目的 public/data 目录。移除前先暂存控件内的子对象,移除控件后再按原顺序插回父对象,内容才不会丢失或错位。
移除过程涉及以下成员:
| 成员 | 作用 |
|---|---|
SDTContent.ChildObjects |
控件内部的内容对象 |
ChildObjects.Remove(child) |
从父对象中移除控件 |
ChildObjects.Insert(index, obj) |
按指定位置插回内容 |
Body.ChildObjects |
节正文的对象集合,块级控件的父对象 |
具体操作步骤如下:
- 获取 Spire.Doc 的 WASM 模块,确认已就绪。
- 载入字体,把待处理的
RemoveContentControls.docx读入 VFS 并加载为Document。 - 遍历正文对象:对段落中的行内控件,暂存其内容,移除控件后把内容插回段落。
- 对块级控件,暂存其内容,移除控件后把内容插回节正文。
- 使用
SaveToFile()保存结果文件,从 VFS 读出并触发浏览器下载。
移除内容控件并保留内容的代码如下:
function App() {
const removeContentControls = async () => {
// 获取 Spire.Doc 的 WASM 模块
const wasmModule = window.wasmModule?.spiredoc;
if (!wasmModule) {
alert('Spire.Doc is not ready yet');
return;
}
// 载入待处理的文档
await window.spire.FetchFileToVFS('RemoveContentControls.docx', '', `${process.env.PUBLIC_URL}/data/`);
const doc = new wasmModule.Document();
doc.LoadFromFile('RemoveContentControls.docx');
// 遍历各节
for (let s = 0; s < doc.Sections.Count; s++) {
const section = doc.Sections.get_Item(s);
for (let i = 0; i < section.Body.ChildObjects.Count; i++) {
const obj = section.Body.ChildObjects.get_Item(i);
// 处理段落中的行内控件
if (obj.DocumentObjectType === wasmModule.DocumentObjectType.Paragraph) {
for (let j = 0; j < obj.ChildObjects.Count; j++) {
const child = obj.ChildObjects.get_Item(j);
if (child.DocumentObjectType !== wasmModule.DocumentObjectType.StructureDocumentTagInline) continue;
// 暂存控件内容
const inner = [];
for (let k = 0; k < child.SDTContent.ChildObjects.Count; k++) {
inner.push(child.SDTContent.ChildObjects.get_Item(k));
}
// 移除控件,再把内容按原位置插回段落
obj.ChildObjects.Remove(child);
for (let n = 0; n < inner.length; n++) {
obj.ChildObjects.Insert(j + n, inner[n]);
}
j += inner.length;
}
}
// 处理块级控件
if (obj.DocumentObjectType === wasmModule.DocumentObjectType.StructureDocumentTag) {
// 暂存控件内容
const inner = [];
for (let k = 0; k < obj.SDTContent.ChildObjects.Count; k++) {
inner.push(obj.SDTContent.ChildObjects.get_Item(k));
}
// 移除控件,再把内容按原位置插回节正文
for (let n = 0; n < inner.length; n++) {
section.Body.ChildObjects.Insert(i + n, inner[n]);
}
section.Body.ChildObjects.Remove(obj);
i += inner.length;
}
}
}
// 保存结果文件
const outputFileName = 'RemoveContentControls_out.docx';
doc.SaveToFile({ fileName: outputFileName, fileFormat: wasmModule.FileFormat.Docx2013 });
doc.Dispose();
// 从 VFS 读取生成的文件,触发下载
const fileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
const blob = new Blob([fileArray], { type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = outputFileName;
a.click();
URL.revokeObjectURL(url);
};
return (
<div style={{ textAlign: 'center', height: '300px' }}>
<h1>移除内容控件并保留内容</h1>
<button onClick={removeContentControls}>生成文档</button>
</div>
);
}
export default App;
处理前后的对比如下,左图为含内容控件的原始文档,右图为移除控件后的文档:

行内与块级控件的移除差异
行内控件只是段落中的一个子对象,移除它不会影响段落结构,内容插回段落即可;块级控件本身占据正文的一整段位置,移除后需要把它的内容直接插回 Body.ChildObjects。示例分别用 j += inner.length 与 i += inner.length 跳过刚插回的内容,避免重复处理。
如果控件是空的,inner 长度为 0,循环不会执行,控件被直接移除,不会留下空白段落。若希望连内容一起删除,省略插回内容的循环即可;但要注意,块级控件被移除后,原先由它撑起的段落间距也会一并消失。
常见问题
锁定内容控件后还能用代码修改内容吗
锁定设置由 Word 在编辑界面执行,约束的是人工操作;通过对象模型读取、修改文档不受这些标记限制。若业务上需要严格防止修改,应结合文档保护或只读模式,而不是只依赖内容控件的锁定。
移除内容控件会丢失里面的文字和表格吗
是否丢失取决于移除方式。示例先把 SDTContent.ChildObjects 中的对象暂存到数组,移除控件后再插回父对象,因此文字、表格和格式都会保留,只是不再带有控件外壳。如果直接移除控件而不插回内容,里面的内容会随之丢失。
块级控件移除后内容位置为什么会错乱
块级控件的父对象是节正文,插入位置要按 Body.ChildObjects 的索引计算。若先移除控件再按原索引插入,后续对象的索引已经前移,内容就会落到错误位置。示例先在原位置插入内容、再移除控件,并跳过已处理的对象,顺序才能保持一致。
内容控件锁定与文档保护有什么区别
内容控件锁定只作用于单个控件,粒度细,不影响文档其他部分;文档保护作用于整篇文档,通常限制编辑区域或强制只读。前者适合在可填写的表单中保护个别定稿字段,后者适合整份合同的签署后归档。两者可以同时使用。
获取免费许可证
如果您希望删除结果文档中的评估消息,或者摆脱功能限制,请该Email地址已收到反垃圾邮件插件保护。要显示它您需要在浏览器中启用JavaScript。获取有效期 30 天的临时许可证。







