导出自定义样式到 .docx

Available in Start planBetav0.13.0

在导出为 DOCX 时,您可以定义将应用于导出文档的自定义样式。这在您想要跨文档保持一致的外观和感觉时非常有用。

// 导入 ExportDocx 扩展
import { ExportDocx } from '@tiptap-pro/extension-export-docx'

const editor = new Editor({
  extensions: [
    // 其他扩展 ...
    ExportDocx.configure({
      onCompleteExport: (result: string | Buffer<ArrayBufferLike> | Blob | Stream) => {}, // 必需
      styleOverrides: { // 样式覆盖
        paragraphStyles: [
          // Heading 1 样式覆盖
          {
            id: 'Heading1',
            name: 'Heading 1',
            basedOn: 'Normal',
            next: 'Normal',
            quickFormat: true,
            run: {
              font: 'Aptos',
              size: pointsToHalfPoints(16),
              bold: true,
              color: 'FF0000',
            },
            paragraph: {
              spacing: {
                before: pointsToTwips(12),
                after: pointsToTwips(6),
                line: lineHeightToDocx(1.15),
              },
            },
          },
        ]
      }
    }),
    // 其他扩展 ...
  ],
  // 其他编辑器设置 ...
})

在上面的示例中,我们正在导出一个带有自定义 Heading 1 样式的文档。该样式基于 Normal 样式,使用红色和 Aptos 字体。段落前的间距设置为 12pt,段落后为 6pt。行高设置为 1.15

你还可以为其他元素创建自定义样式,如 Heading 2Heading 3List BulletList Number 等。paragraphStyles 数组接受具有以下属性的对象数组:

段落样式对象

paragraphStyle 对象接受以下属性:

属性类型描述
idstring样式的唯一标识符。
namestring样式的显示名称。
basedOnstring此样式基于的基础样式(例如 Normal)。
nextstring应用于下一个段落的样式。
quickFormatboolean如果为 true,该样式会出现在快速格式菜单中。
runobject定义文本格式(字体、大小、颜色、粗体等)。
paragraphobject定义段落格式(间距、对齐、边框等)。

Run 样式属性

来自 paragraphStylerun 对象接受以下属性:

属性类型描述
fontstring文本的字体族。
sizenumber以半点为单位的字体大小(例如,16 点 = 32)。
boldboolean设置为 true 使文本加粗。
italicsboolean设置为 true 使用斜体文本。
colorstring十六进制格式的文本颜色(例如,FF0000 表示红色。不需要 #)。
kernnumber以点为单位调整字符之间的间距。
effect可以应用的特殊文本效果。
emphasisMarkstring出现在文本上方或下方的强调标记。(如 dot
smallCapsboolean设置为 true 以小大写字母显示文本。
allCapsboolean设置为 true 以大写字母显示文本。
strikeboolean设置为 true 应用单删除线。
doubleStrikeboolean设置为 true 应用双删除线。
subScriptboolean设置为 true 使用下标文本。
superScriptboolean设置为 true 使用上标文本。
highlight高亮颜色(预定义值)。
characterSpacingnumber以 TWIP 为单位调整字符之间的间距(…我们知道,TWIP 对吧?
shadingobject对文本应用背景阴影。
shadingtypeShadingType阴影类型(clearsolidhorizontalStripe 等)。
shadingfillstring十六进制格式的阴影填充颜色(例如,FF0000 表示红色)。
shadingcolorstring十六进制格式的阴影颜色(例如,FF0000 表示红色)。
scalenumber调整文本宽度(以百分比为单位)。
underlineobject下划线样式,指定如 colortype 等属性,或使用空对象表示简单下划线。
underlinecolorstring十六进制格式的下划线颜色(例如,FF0000 表示红色。不需要 #)。
underlinetypeUnderlineType下划线类型(singledoublethick

有关更高级的样式选项和详细用法,你可以参考我们包中暴露的 IRunStylePropertiesOptions 类型,或参考 docx 文档

段落样式属性

来自 paragraphStyleparagraph 对象接受以下属性:

属性类型描述
spacingobject控制间距:如 beforeafterline 等属性。
alignmentstring设置段落对齐方式(leftcenterrightjustify)。
borderobject定义段落周围的边框(上、下、左、右)。
shadingobject对段落应用背景阴影。
indentobject指定缩进(首行、悬挂、左、右)。
contextualSpacingboolean如果为 true,减少相同样式段落之间的间距。
keepNextboolean将此段落与下一个段落保持在同一页面上。
keepLinesboolean将段落的所有行保持在同一页面上。
outlineLevelnumber设置文档组织的大纲级别(通常为 1–9)。
thematicBreaknumber设置为 true 时添加水平分页线。
rightTabStopnumber设置右制表位的位置(以 twips 为单位)。
leftTabStopnumber设置左制表位的位置(以 twips 为单位)。
numberingobject控制编号设置(例如引用、级别、自定义格式)。
numberingreferencestring编号样式引用 ID。
numberinglevelnumber编号层次结构中的级别(从 0 开始)。
spacingobject控制段落的间距。
spacingbeforenumber段落前的间距。
spacingafternumber段落后的间距。
spacinglinenumber段落内的行间距。
spacinglineRuleLineRuleType定义如何计算行间距。

有关更高级的样式选项和详细用法,你可以参考我们包中暴露的 IParagraphStylePropertiesOptions 类型,或参考 docx 文档

Tiptap 的导出默认样式

Tiptap 为导出的文档提供了合理的默认样式,但你可以通过提供自己的自定义样式来覆盖这些样式。这使你可以在整个文档中创建一致的外观和感觉。

{
  paragraphStyles: [
    // Normal 样式(大多数段落的默认样式)
    {
      id: 'Normal',
      name: 'Normal',
      run: {
        font: 'Aptos',
        size: pointsToHalfPoints(11),
      },
      paragraph: {
        spacing: {
          before: 0,
          after: pointsToTwips(10),
          line: lineHeightToDocx(1.15),
        },
      },
    },
    // List Paragraph 样式(用于项目符号和编号)
    {
      id: 'ListParagraph',
      name: 'List Paragraph',
      basedOn: 'Normal',
      quickFormat: true,
      run: {
        font: 'Aptos',
        size: pointsToHalfPoints(11),
      },
      paragraph: {
        spacing: {
          before: 0,
          after: pointsToTwips(2),
          line: lineHeightToDocx(1),
        },
      },
    },
    // Heading 1 样式
    {
      id: 'Heading1',
      name: 'Heading 1',
      basedOn: 'Normal',
      next: 'Normal',
      quickFormat: true,
      run: {
        font: 'Aptos Light',
        size: pointsToHalfPoints(16),
        bold: true,
        color: '2E74B5',
      },
      paragraph: {
        spacing: {
          before: pointsToTwips(12),
          after: pointsToTwips(6),
          line: lineHeightToDocx(1.15),
        },
      },
    },
    // Heading 2 样式
    {
      id: 'Heading2',
      name: 'Heading 2',
      basedOn: 'Normal',
      next: 'Normal',
      quickFormat: true,
      run: {
        font: 'Aptos Light',
        size: pointsToHalfPoints(14),
        bold: true,
        color: '2E74B5',
      },
      paragraph: {
        spacing: {
          before: pointsToTwips(12),
          after: pointsToTwips(6),
          line: lineHeightToDocx(1.15),
        },
      },
    },
    // Heading 3 样式
    {
      id: 'Heading3',
      name: 'Heading 3',
      basedOn: 'Normal',
      next: 'Normal',
      quickFormat: true,
      run: {
        font: 'Aptos',
        size: pointsToHalfPoints(13),
        bold: true,
        color: '2E74B5',
      },
      paragraph: {
        spacing: {
          before: pointsToTwips(12),
          after: pointsToTwips(6),
          line: lineHeightToDocx(1.15),
        },
      },
    },
    // Heading 4 样式
    {
      id: 'Heading4',
      name: 'Heading 4',
      basedOn: 'Normal',
      next: 'Normal',
      quickFormat: true,
      run: {
        font: 'Aptos',
        size: pointsToHalfPoints(12),
        bold: true,
        color: '2E74B5',
      },
      paragraph: {
        spacing: {
          before: pointsToTwips(12),
          after: pointsToTwips(6),
          line: lineHeightToDocx(1.15),
        },
      },
    },
    // Heading 5 样式
    {
      id: 'Heading5',
      name: 'Heading 5',
      basedOn: 'Normal',
      next: 'Normal',
      quickFormat: true,
      run: {
        font: 'Aptos',
        size: pointsToHalfPoints(11),
        bold: true,
        color: '2E74B5',
      },
      paragraph: {
        spacing: {
          before: pointsToTwips(12),
          after: pointsToTwips(6),
          line: lineHeightToDocx(1.15),
        },
      },
    },
    // Title 样式
    {
      id: 'Title',
      name: 'Title',
      basedOn: 'Normal',
      next: 'Normal',
      quickFormat: true,
      run: {
        font: 'Aptos Light',
        size: pointsToHalfPoints(22),
        bold: true,
        color: '000000',
      },
      paragraph: {
        alignment: AlignmentType.CENTER,
        spacing: {
          before: 0,
          after: 0,
          line: lineHeightToDocx(1.15),
        },
      },
    },
    // Subtitle 样式
    {
      id: 'Subtitle',
      name: 'Subtitle',
      basedOn: 'Normal',
      next: 'Normal',
      quickFormat: true,
      run: {
        font: 'Aptos Light',
        size: pointsToHalfPoints(16),
        italics: true,
        color: '666666',
      },
      paragraph: {
        alignment: AlignmentType.CENTER,
        spacing: {
          before: 0,
          after: 0,
          line: lineHeightToDocx(1.15),
        },
      },
    },
    // Quote 样式(通常用于缩进的斜体文本)
    {
      id: 'Quote',
      name: 'Quote',
      basedOn: 'Normal',
      quickFormat: true,
      run: {
        font: 'Aptos',
        italics: true,
      },
      paragraph: {
        alignment: AlignmentType.CENTER,
        spacing: {
          before: pointsToTwips(10),
          after: pointsToTwips(10),
          line: lineHeightToDocx(1.15),
        },
      },
    },
    // Intense Quote 样式(更明显的缩进)
    {
      id: 'IntenseQuote',
      name: 'Intense Quote',
      basedOn: 'Normal',
      quickFormat: true,
      run: {
        font: 'Aptos',
        italics: true,
        color: '444444',
      },
      paragraph: {
        alignment: AlignmentType.CENTER,
        spacing: {
          before: pointsToTwips(10),
          after: pointsToTwips(10),
          line: lineHeightToDocx(1.15),
        },
      },
    },
    // No Spacing 样式(段落前后无额外间距)
    {
      id: 'NoSpacing',
      name: 'No Spacing',
      basedOn: 'Normal',
      quickFormat: true,
      paragraph: {
        spacing: {
          before: 0,
          after: 0,
          line: lineHeightToDocx(1),
        },
      },
    },
    // Hyperlink 样式
    {
      id: 'Hyperlink',
      name: 'Hyperlink',
      basedOn: 'Normal',
      run: {
        color: '0563C1',
        underline: {
          type: 'single',
        },
      },
    },
  ],
}

元素覆盖

元素覆盖与 styleOverrides 是分开的。虽然 styleOverrides 定义了 Word 通过其样式系统应用的命名段落样式(标题 1、正文等),但元素覆盖在转换过程中直接将基础默认值应用于每个段落、文本运行、表格、表格单元格或图像。

这让您无需定义命名样式即可对每种元素的原始 docx 属性进行细粒度控制。

浅层展开:所有覆盖对象都使用浅层展开,而非深层合并。在覆盖如 spacingtransformation 等嵌套属性时,请提供完整的嵌套对象,以避免出现未定义字段。

paragraphOverrides

接受除 children 之外的任何 IParagraphOptions 属性。这些值会作为基础默认值展开;每个节点计算出的值(alignment、列表编号、标题级别)会覆盖它们。

应用于:标准段落、标题、块引用、无序列表、有序列表和列表项。

ExportDocx.configure({
  onCompleteExport: (result) => { /* ... */ },
  paragraphOverrides: {
    spacing: { after: 200, before: 100 },
    alignment: AlignmentType.LEFT,
  },
})

spacing 是部分覆盖paragraphOverrides 中的 spacing.beforespacing.after 会保留在每个段落上,但 spacing.line 始终会根据每个段落的 lineHeight 重新计算,并替换您在此处传入的任何值。IParagraphOptions 上其他嵌套对象仍然会被整体替换(不进行深度合并)。

textRunOverrides

接受除 text 之外的任何 IRunOptions 属性。这些值会作为基础默认值展开;按标记的格式设置(粗体、斜体、下划线、字体族、字号、颜色、高亮等)会覆盖它们。

可用于为整个导出的文档设置默认字体或字号。

ExportDocx.configure({
  onCompleteExport: (result) => { /* ... */ },
  textRunOverrides: {
    font: 'Arial',
    size: 24, // 单位为半点数的 12pt
  },
})

tableOverrides

接受除 rows 之外的任何 ITableOptions 属性。这些值会最后展开,因此用户提供的表格级属性(边框、边距、布局)会优先于计算出的默认值。

ExportDocx.configure({
  onCompleteExport: (result) => { /* ... */ },
  tableOverrides: {
    width: { size: 100, type: WidthType.PERCENTAGE },
    layout: TableLayoutType.FIXED,
  },
})

tableCellOverrides

接受除 children 之外的任何 ITableCellOptions 属性。这些值会作为基础默认值展开;每个单元格计算出的值(宽度、colspan、宽度类型)会覆盖它们。

可用于为所有表格单元格应用一致的底纹、垂直对齐或边框样式。

ExportDocx.configure({
  onCompleteExport: (result) => { /* ... */ },
  tableCellOverrides: {
    shading: { fill: 'F0F0F0', type: ShadingType.SOLID },
    verticalAlign: VerticalAlign.CENTER,
  },
})

imageOverrides

接受除 datatypefallback 之外的任何 IImageOptions 属性。这些值会最后展开,因此用户提供的值(例如自定义 transformation 尺寸)会优先于从图像缓冲区检测到的内在尺寸。无论覆盖如何,SVG 回退数据始终会保留。

ExportDocx.configure({
  onCompleteExport: (result) => { /* ... */ },
  imageOverrides: {
    transformation: { width: 400, height: 300 },
    floating: {
      horizontalPosition: { offset: 0 },
      verticalPosition: { offset: 0 },
    },
  },
})

组合使用示例

您可以在单个配置中组合所有元素覆盖:

editor.commands.exportDocx({
  onCompleteExport: (result) => { /* ... */ },
  exportType: 'blob',
  paragraphOverrides: {
    spacing: { after: 200, before: 100 },
  },
  textRunOverrides: {
    font: 'Arial',
    size: 24, // 单位为半点数的 12pt
  },
  tableCellOverrides: {
    shading: { fill: 'F0F0F0', type: ShadingType.SOLID },
  },
  imageOverrides: {
    transformation: { width: 400, height: 300 },
  },
})

headerFooterOverrides

元素覆盖同样适用于页眉和页脚内容以及正文——在顶层设置的 paragraphOverridestextRunOverridestableOverridestableCellOverridesimageOverrides 值也会流入运行中的页眉和页脚。

当您希望页眉/页脚内容与正文不同时,请使用 headerFooterOverrides。它接受相同的五个字段,并替换仅作用于页眉/页脚内容的对应正文级覆盖。每个字段都是完整替换,而不是深度合并——您未定义的字段会回退到同名的正文级覆盖。

import { BorderStyle } from 'docx'

ExportDocx.configure({
  onCompleteExport: (result) => { /* ... */ },
  // 正文表格将获得 1pt 网格边框
  tableOverrides: {
    borders: {
      top:    { style: BorderStyle.SINGLE, size: 4, color: '000000' },
      bottom: { style: BorderStyle.SINGLE, size: 4, color: '000000' },
      left:   { style: BorderStyle.SINGLE, size: 4, color: '000000' },
      right:  { style: BorderStyle.SINGLE, size: 4, color: '000000' },
      insideHorizontal: { style: BorderStyle.SINGLE, size: 4, color: '000000' },
      insideVertical:   { style: BorderStyle.SINGLE, size: 4, color: '000000' },
    },
  },
  // 页眉/页脚表格(例如页面页眉中的布局表格)将
  // 不显示边框。除非在这里覆盖,其他正文覆盖仍然适用。
  headerFooterOverrides: {
    tableOverrides: {
      borders: {
        top:    { style: BorderStyle.NONE },
        bottom: { style: BorderStyle.NONE },
        left:   { style: BorderStyle.NONE },
        right:  { style: BorderStyle.NONE },
        insideHorizontal: { style: BorderStyle.NONE },
        insideVertical:   { style: BorderStyle.NONE },
      },
    },
  },
})

作用范围headerFooterOverrides 仅适用于从 Pages 扩展 自动提取的页眉/页脚内容。如果您使用 Docx.Header / Docx.Footer 实例手动构建页眉/页脚,或者使用传入 headers / footers 选项的自定义工厂函数进行构建,这些构造都会绕过 headerFooterOverrides —— 请在您的工厂函数内部自行应用这些覆盖。