使用页眉和页脚扩展您的 DOCX 导出功能

Available in Start planBetav0.13.0

@tiptap-pro/extension-export-docx 扩展内置支持自定义导出文档的页眉和页脚。

添加此扩展后,您可以通过以下附加属性配置您的 ExportDocx

页眉配置

headers 对象允许您自定义导出 DOCX 文档的页眉:

属性类型描述
evenAndOddHeadersboolean是否为奇数页和偶数页使用不同的页眉
differentFirstPageboolean是否首页使用不同的页眉。当为 true 时,首页使用 first 值,取代 default
defaultHeader | (() => Promise<Header>)每页的标准默认页眉,或当激活 evenAndOddHeaders 选项时用于奇数页的页眉。可以是 Header 对象或返回 Header 的异步函数
firstHeader | (() => Promise<Header>)首页的页眉,仅在 differentFirstPage 设置为 true 时使用。可以是 Header 对象或返回 Header 的异步函数
evenHeader | (() => Promise<Header>)当激活 evenAndOddHeaders 选项时偶数页的页眉。可以是 Header 对象或返回 Header 的异步函数

页脚配置

footers 对象允许您自定义导出 DOCX 文档的页脚:

属性类型描述
evenAndOddFootersboolean是否为奇数页和偶数页使用不同的页脚
differentFirstPageboolean是否首页使用不同的页脚。当为 true 时,首页使用 first 值,取代 default
defaultFooter | (() => Promise<Footer>)每页的标准默认页脚,或当激活 evenAndOddFooters 选项时用于奇数页的页脚。可以是 Footer 对象或返回 Footer 的异步函数
firstFooter | (() => Promise<Footer>)首页的页脚,仅在 differentFirstPage 设置为 true 时使用。可以是 Footer 对象或返回 Footer 的异步函数
evenFooter | (() => Promise<Footer>)当激活 evenAndOddFooters 选项时偶数页的页脚。可以是 Footer 对象或返回 Footer 的异步函数

使用示例

扩展配置

通过 ExportDocx.configure() 方法配置页眉和页脚。您可以直接使用 Docx 命名空间中的对象,如 Docx.HeaderDocx.Footer,以获得完全自定义的控制:

关于页眉和页脚对象的说明

HeaderFooterParagraphTextRun 对象通过 @tiptap-pro/extension-export-docx 导出的 Docx 命名空间访问。您可以使用任何有效内容自定义它们,包括段落、文本运行等,只要是在 DOCX 页眉或页脚中可用的标准元素。

import { ExportDocx, Docx } from '@tiptap-pro/extension-export-docx'

const editor = new Editor({
  extensions: [
    // ... 其他扩展
    ExportDocx.configure({
      onCompleteExport: result => {
        // 处理导出结果
        const blob = new Blob([result], {
          type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
        })
        const url = URL.createObjectURL(blob)
        const a = document.createElement('a')
        a.href = url
        a.download = 'export.docx'
        a.click()
        URL.revokeObjectURL(url)
      },
      headers: {
        evenAndOddHeaders: true,
        default: new Docx.Header({
          children: [
            new Docx.Paragraph({
              children: [
                new Docx.TextRun({
                  text: "默认页眉",
                  bold: true,
                }),
              ],
            }),
          ],
        }),
        first: new Docx.Header({
          children: [
            new Docx.Paragraph({
              children: [
                new Docx.TextRun({
                  text: "首页页眉",
                  size: 24,
                  bold: true,
                }),
              ],
            }),
          ],
        }),
        even: new Docx.Header({
          children: [
            new Docx.Paragraph({
              children: [
                new Docx.TextRun({
                  text: "偶数页页眉",
                }),
              ],
            }),
          ],
        }),
      },
      footers: {
        default: new Docx.Footer({
          children: [
            new Docx.Paragraph({
              children: [
                new Docx.TextRun({
                  text: "默认页脚",
                }),
              ],
            }),
          ],
        }),
      },
    }),
  ],
})

// 触发导出
editor.commands.exportDocx()

使用辅助函数(替代方法)

为方便处理 Tiptap 风格内容,您可以使用 convertHeaderconvertFooter 辅助函数,它们会自动处理 mark 转换、链接和其他 Tiptap 特性。

import { ExportDocx } from '@tiptap-pro/extension-export-docx'

const editor = new Editor({
  extensions: [
    // ... 其他扩展
    ExportDocx.configure({
      onCompleteExport: result => {
        // 处理导出结果
        const blob = new Blob([result], {
          type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
        })
        const url = URL.createObjectURL(blob)
        const a = document.createElement('a')
        a.href = url
        a.download = 'export.docx'
        a.click()
        URL.revokeObjectURL(url)
      },
      headers: {
        evenAndOddHeaders: true,
        default: async () =>
          convertHeader({
            node: {
              type: 'paragraph',
              content: [
                {
                  type: 'text',
                  text: '页眉',
                  marks: [{ type: 'textStyle', attrs: { color: 'red' } }],
                },
              ],
            },
          }),
        first: async () =>
          convertHeader({
            node: {
              type: 'paragraph',
              content: [
                {
                  type: 'text',
                  text: '首页页眉',
                  marks: [{ type: 'bold' }],
                },
              ],
            },
          }),
        even: async () =>
          convertHeader({
            node: {
              type: 'paragraph',
              content: [
                {
                  type: 'text',
                  text: '偶数页页眉',
                  marks: [{ type: 'textStyle', attrs: { color: 'blue' } }],
                },
              ],
            },
          }),
      },
      footers: {
        default: async () =>
          convertFooter({
            node: {
              type: 'paragraph',
              content: [
                {
                  type: 'text',
                  text: '页脚',
                  marks: [{ type: 'textStyle', attrs: { color: 'red' } }],
                },
              ],
            },
          }),
        first: async () =>
          convertFooter({
            node: {
              type: 'paragraph',
              content: [
                {
                  type: 'text',
                  text: '首页页脚',
                  marks: [{ type: 'bold' }],
                },
              ],
            },
          }),
        even: async () =>
          convertFooter({
            node: {
              type: 'paragraph',
              content: [
                {
                  type: 'text',
                  text: '偶数页页脚',
                  marks: [{ type: 'textStyle', attrs: { color: 'blue' } }],
                },
              ],
            },
          }),
      },
    }),
  ],
})

// 触发导出
editor.commands.exportDocx()

convertHeaderconvertFooter 辅助函数期望传入一个 Tiptap 节点,这里为一个段落,它们会自动处理:

  • Mark 转换(加粗、斜体、颜色等)
  • 链接转换
  • 文本样式和格式
  • 复杂的 Tiptap 节点结构

这使得使用熟悉的 Tiptap JSON 结构创建丰富的页眉和页脚变得更简单。

重要提示!

请注意,如果您使用 convertHeaderconvertFooter 辅助函数,则需要使用异步箭头函数,因为这些辅助工具函数内部调用了异步函数 convertParagraph,其异步性是为了实现图像的解析处理。

高级示例

使用异步函数的动态页眉和页脚

您也可以提供返回 HeaderFooter 对象的异步函数。这对于动态内容生成非常有用,例如获取用户数据、格式化当前日期或处理图像。

异步函数的优点

使用异步函数定义页眉和页脚,可以实现强大的用例,例如:从 API 获取用户信息,包含当前时间戳或动态日期,处理和嵌入图像,计算文档统计信息,从数据库或外部服务检索数据。

import { ExportDocx, Docx } from '@tiptap-pro/extension-export-docx'

// 获取用户信息的函数
async function getCurrentUser() {
  // 这里可以是 API 调用、数据库查询等
  return {
    name: 'John Doe',
    department: '工程部',
    email: 'john.doe@company.com'
  }
}

const editor = new Editor({
  extensions: [
    // ... 其他扩展
    ExportDocx.configure({
      onCompleteExport: result => {
        // 处理导出结果
        const blob = new Blob([result], {
          type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
        })
        const url = URL.createObjectURL(blob)
        const a = document.createElement('a')
        a.href = url
        a.download = 'export.docx'
        a.click()
        URL.revokeObjectURL(url)
      },
      headers: {
        default: async () => {
          const user = await getCurrentUser()
          const currentDate = new Date().toLocaleDateString()
          
          return new Docx.Header({
            children: [
              new Docx.Paragraph({
                children: [
                  new Docx.TextRun({
                    text: `文档由 ${user.name} (${user.department}) 于 ${currentDate} 准备`,
                    size: 20,
                  }),
                ],
              }),
            ],
          })
        },
      },
      footers: {
        default: async () => {
          const user = await getCurrentUser()
          
          return new Docx.Footer({
            children: [
              new Docx.Paragraph({
                children: [
                  new Docx.TextRun({
                    text: `联系方式: ${user.email} | 生成时间: ${new Date().toISOString()}`,
                    size: 16,
                    italics: true,
                  }),
                ],
              }),
            ],
          })
        },
      },
    }),
  ],
})

// 触发导出
editor.commands.exportDocx()

异步函数的生命周期

提供的异步函数将在整个文档转换完成后、构建文档之前调用。

静态和动态页眉/页脚混合使用

您可以将静态页眉/页脚和动态页眉/页脚结合使用于不同页面:

import { ExportDocx, Docx } from '@tiptap-pro/extension-export-docx'

const editor = new Editor({
  extensions: [
    // ... 其他扩展
    ExportDocx.configure({
      onCompleteExport: result => {
        // 处理导出结果
        const blob = new Blob([result], {
          type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
        })
        const url = URL.createObjectURL(blob)
        const a = document.createElement('a')
        a.href = url
        a.download = 'export.docx'
        a.click()
        URL.revokeObjectURL(url)
      },
      headers: {
        // 静态首页页眉
        first: new Docx.Header({
          children: [
            new Docx.Paragraph({
              children: [
                new Docx.TextRun({
                  text: "公司机密报告",
                  size: 24,
                  bold: true,
                }),
              ],
            }),
          ],
        }),
        // 后续页的动态默认页眉
        default: async () => {
          const reportData = await fetchReportMetadata()
          
          return new Docx.Header({
            children: [
              new Docx.Paragraph({
                children: [
                  new Docx.TextRun({
                    text: `${reportData.title} - 生成于 ${reportData.timestamp}`,
                    size: 18,
                  }),
                ],
              }),
            ],
          })
        },
      },
      footers: {
        default: async () => {
          const stats = await getDocumentStats()
          
          return new Docx.Footer({
            children: [
              new Docx.Paragraph({
                children: [
                  new Docx.TextRun({
                    text: `页数: ${stats.pages} | 字数: ${stats.words}`,
                    size: 14,
                  }),
                ],
              }),
            ],
          })
        },
      },
    }),
  ],
})

async function fetchReportMetadata() {
  // 模拟 API 调用
  return {
    title: '季度业务报告',
    timestamp: new Date().toLocaleDateString()
  }
}

async function getDocumentStats() {
  // 模拟文档分析
  return {
    pages: 10,
    words: 2500
  }
}

页码

导出支持两种方式来生成实时页码字段:

  1. 普通文本页眉和页脚中的 {page} / {total} 标记——可与从 Pages 扩展 中提取的内容自动配合使用,也可与传递给 headers / footers 的普通文本字符串一起使用。导出会将每个标记转换为实时的 PAGE / NUMPAGES 字段,因此 Word 会渲染出实际的当前页码和总页数。
  2. Docx.PageNumber API——当你使用 new Docx.Header() / new Docx.Footer() 手动构建页眉/页脚内容时,或者当你需要在字段周围加入超出纯标记之外的格式时,这是必需的。convertHeader / convertFooter 辅助函数目前不会处理 Docx.PageNumber 引用——请直接传入 Docx 实例。

通过 {page} / {total} 标记显示页码

任何普通文本页眉或页脚(包括通过 Pages 扩展自动提取的内容,例如 editor.commands.setHeader('Page {page} of {total}'))在导出的 DOCX 中都会渲染为实时页码字段:

import { ExportDocx } from '@tiptap-pro/extension-export-docx'

const editor = new Editor({
  extensions: [
    // ... 其他扩展
    ExportDocx.configure({
      onCompleteExport: result => {
        const blob = new Blob([result], {
          type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
        })
        const url = URL.createObjectURL(blob)
        const a = document.createElement('a')
        a.href = url
        a.download = 'export.docx'
        a.click()
        URL.revokeObjectURL(url)
      },
      // 普通文本页眉/页脚——`{page}` 和 `{total}` 会在导出时转换为
      // 实时的 `PAGE` / `NUMPAGES` 字段。
      headers: {
        default: 'Confidential — Page {page} of {total}',
      },
      footers: {
        default: 'Page {page} of {total}',
      },
    }),
  ],
])

// 触发导出
editor.commands.exportDocx()

当页眉/页脚是从 Pages 扩展中自动提取时,也会自动执行相同的转换,因此通过 editor.commands.setFooter('Page {page} of {total}') 设置的页脚在导出时会包含实时页码,无需任何额外配置。

基本页码

import { ExportDocx, Docx } from '@tiptap-pro/extension-export-docx'

const editor = new Editor({
  extensions: [
    // ... 其他扩展
    ExportDocx.configure({
      onCompleteExport: result => {
        // 处理导出结果
        const blob = new Blob([result], {
          type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
        })
        const url = URL.createObjectURL(blob)
        const a = document.createElement('a')
        a.href = url
        a.download = 'export.docx'
        a.click()
        URL.revokeObjectURL(url)
      },
      headers: {
        default: new Docx.Header({
          children: [
            new Docx.Paragraph({
              children: [
                new Docx.TextRun({
                  children: ['第 ', Docx.PageNumber.CURRENT, ' 页'],
                }),
              ],
            }),
          ],
        }),
      },
      footers: {
        default: new Docx.Footer({
          children: [
            new Docx.Paragraph({
              children: [
                new Docx.TextRun({
                  children: ['第 ', Docx.PageNumber.CURRENT, ' 页'],
                }),
              ],
            }),
          ],
        }),
      },
    }),
  ],
])

// 触发导出
editor.commands.exportDocx()

显示当前页码和总页数

import { ExportDocx, Docx } from '@tiptap-pro/extension-export-docx'

const editor = new Editor({
  extensions: [
    // ... 其他扩展
    ExportDocx.configure({
      onCompleteExport: result => {
        // 处理导出结果
        const blob = new Blob([result], {
          type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
        })
        const url = URL.createObjectURL(blob)
        const a = document.createElement('a')
        a.href = url
        a.download = 'export.docx'
        a.click()
        URL.revokeObjectURL(url)
      },
      footers: {
        default: new Docx.Footer({
          children: [
            new Docx.Paragraph({
              children: [
                new Docx.TextRun({
                  children: [
                    '第 ',
                    Docx.PageNumber.CURRENT,
                    ' 页,共 ',
                    Docx.PageNumber.TOTAL_PAGES,
                    ' 页',
                  ],
                }),
              ],
            }),
          ],
        }),
      },
    }),
  ],
])

// 触发导出
editor.commands.exportDocx()