返回 导航

Vue.js

hangge.com

Vue.js - 富文本编辑器Tiptap使用详解13(将内容导出为Word)

作者:hangge | 2026-03-02 08:43
    有时我们需要导出 Tiptap 内容为 Word (.docx),本文演示如何借助 html-docx-js + FileSaver 来快速实现纯前端导出,以及通过后端进行导出这两种方法。

十三、将内容导出为 Word(客户端导出)

1,安装依赖

要实现客户端导出,我们需要先安装 html-docx-jsfile-saver 这两个依赖库。
npm install html-docx-js file-saver

2,样例代码

<template>
  <div class="p-4">
    <div class="mb-3 flex gap-2">
      <button @click="exportDocx" class="btn">导出为 Word</button>
    </div>
    <div class="editor-box border rounded">
      <editor-content :editor="editor" />
    </div>
  </div>
</template>

<script>
import { Editor } from '@tiptap/core'
import StarterKit from '@tiptap/starter-kit'
import { EditorContent } from '@tiptap/vue-2'
import Link from '@tiptap/extension-link'
import Image from '@tiptap/extension-image'
// 推荐:TableKit 一次引入所有 table 相关扩展
import { TableKit } from '@tiptap/extension-table'
import { TextStyleKit }   from '@tiptap/extension-text-style'
import { Color } from '@tiptap/extension-color'
import Highlight from '@tiptap/extension-highlight'

import htmlDocx from 'html-docx-js/dist/html-docx' // html-docx-js
import { saveAs } from 'file-saver'

export default {
  components: { EditorContent },
  data() { return { editor: null } },
  mounted() {
    this.editor = new Editor({
      extensions: [StarterKit, Link, Image, TableKit, TextStyleKit,
        Color.configure({
            types: ['textStyle'],
        }),
        // multicolor: true 允许自定义颜色
        Highlight.configure({
          multicolor: true,
        }),
      ],
      content: `
        <p>欢迎访问<a href="https://www.hangge.com">hangge.com</a></p>
        <p><img src="https://www.hangge.com/blog/images/logo.png" alt="logo"></p>
        <p></p>
        <p>测试<strong>加粗</strong>测试<em>斜体</em>测试<u>下划线</u>测试<s>删除线</s></p>
        <p></p>
        <p><span style="color: rgb(255, 102, 255);">修改文字颜色</span></p>
        <p><span style="font-size: 22px;">调整文字大小</span></p>
        <p></p>
        <p>下面是一个3x3表格</p>
        <table>
          <thead>
            <tr><th>ID</th><th>用户名称</th><th>用户年龄</th></tr>
          </thead>
          <tbody>
            <tr><td>1</td><td>刘小龙</td><td>88</td></tr>
            <tr><td>2</td><td>张大伟</td><td>66</td></tr>
          </tbody>
        </table>
      `,
    })
  },
  methods: {
    exportDocx() {
      const html = `
        <html>
          <head>
            <meta charset="utf-8">
            <style>
              /* 控制导出样式(简化) */
              body { font-family: "Arial", sans-serif; }
              table { border-collapse: collapse; }
              th { background: #F9FAFB; font-weight: 600; }
              td, th { border: 1px solid #ccc; padding: 4px; }
            </style>
          </head>
          <body>${this.editor.getHTML()}</body>
        </html>
      `
      // html-docx-js: 返回 ArrayBuffer / Blob
      const converted = htmlDocx.asBlob(html) // 注意:不同版本 API 可能是 asBlob / asHTML
      saveAs(converted, 'document.docx')
    }
  },
  beforeDestroy() { this.editor?.destroy() }
}
</script>
<style scoped>
.btn { padding:6px 10px; border-radius:4px; border:1px solid #ddd; background:#fff; cursor:pointer }
.editor-box { min-height: 220px; padding: 12px; background:#fff; }

/* ====== 关键:为编辑器内 table 添加样式 ====== */
/* Vue2 scoped 下使用深度选择器确保样式能命中 ProseMirror 渲染的元素 */
/* 如果你的环境不识别 ::v-deep,请试试 >>> 或 /deep/ */
::v-deep .ProseMirror table {
  width: 100%;
  border-collapse: collapse;
  table-layout: auto;
  margin: 6px 0;
}

/* 单元格边框与内边距 */
::v-deep .ProseMirror th,
::v-deep .ProseMirror td {
  border: 1px solid #d1d5db; /* 灰色边框,可按需调整 #e5e7eb #cbd5e1 等 */
  padding: 8px 10px;
  vertical-align: middle;
}

/* 表头样式(可选) */
::v-deep .ProseMirror th {
  background: #f9fafb;
  font-weight: 600;
}

/* 表格在窄屏时不要换行太多(可选) */
::v-deep .ProseMirror td {
  word-break: break-word;
}

/* 如果使用 Tailwind 的 prose 可能会有默认的 table 样式,下面加个更高权重以覆盖 */
::v-deep .ProseMirror table,
::v-deep .ProseMirror th,
::v-deep .ProseMirror td {
  /* 提高优先级避免被 reset 覆盖 */
  border-color: #d1d5db;
}

/* Tiptap 表格单元格被选中时的高亮效果(与官网一致) */
::v-deep .ProseMirror .selectedCell {
  background-color: rgba(200, 200, 255, 0.4) !important;
  /* 防止 reset 覆盖 */
}

/** 下面是解决列宽拖拽调整时鼠标样式问题的 CSS */
/* 提升优先级:靠右侧 5~8px 时鼠标应变成 col-resize */
::v-deep .ProseMirror table td,
::v-deep .ProseMirror table th {
  position: relative;
}
::v-deep .ProseMirror table td::after,
::v-deep .ProseMirror table th::after {
  content: "";
  position: absolute;
  right: -3px;     /* 命中区域 */
  top: 0;
  width: 6px;
  height: 100%;
  cursor: col-resize !important;
}
</style>

3,运行测试

(1)打开页面,点击“导出为 Word”按钮即可生成 docx 格式的 Word 文件。

(2)打开文件可以看到里面内容如下:

附:通过后端进行将内容导出为 Word

1,实现原理

    前端把 editor.getHTML()(或带全套 head/styles 的完整 HTMLPOST 到后端 → 后端用 docx4j 生成返回文档流 → 前端触发下载。

2,后端服务接口准备

    这里我们使用 SpringBoot 项目作为后台服务,其提供 /export-docx 接口,接收 JSON { html: "...", filename?: "xxx.docx" },返回 application/vnd.openxmlformats-officedocument.wordprocessingml.document 二进制流。具体实现参考我们之前文章:

3,前端代码

<template>
  <div class="p-4">
    <div class="mb-3 flex gap-2">
      <button @click="exportViaServer" class="btn">服务端导出为 Word</button>
    </div>
    <div class="editor-box border rounded">
      <editor-content :editor="editor" />
    </div>
  </div>
</template>

<script>
import { Editor } from '@tiptap/core'
import StarterKit from '@tiptap/starter-kit'
import { EditorContent } from '@tiptap/vue-2'
import Link from '@tiptap/extension-link'
import Image from '@tiptap/extension-image'
// 推荐:TableKit 一次引入所有 table 相关扩展
import { TableKit } from '@tiptap/extension-table'
import { TextStyleKit }   from '@tiptap/extension-text-style'
import { Color } from '@tiptap/extension-color'
import Highlight from '@tiptap/extension-highlight'

export default {
  components: { EditorContent },
  data() { return { editor: null } },
  mounted() {
    this.editor = new Editor({
      extensions: [StarterKit, Link, Image, TableKit, TextStyleKit,
        Color.configure({
            types: ['textStyle'],
        }),
        // multicolor: true 允许自定义颜色
        Highlight.configure({
          multicolor: true,
        }),
      ],
      content: `
        <p>欢迎访问<a href="https://www.hangge.com">hangge.com</a></p>
        <p><img src="https://www.hangge.com/blog/images/logo.png" alt="logo"></p>
        <p></p>
        <p>测试<strong>加粗</strong>测试<em>斜体</em>测试<u>下划线</u>测试<s>删除线</s></p>
        <p></p>
        <p><span style="color: rgb(255, 102, 255);">修改文字颜色</span></p>
        <p><span style="font-size: 22px;">调整文字大小</span></p>
        <p></p>
        <p>下面是一个3x3表格</p>
        <table>
          <thead>
            <tr><th>ID</th><th>用户名称</th><th>用户年龄</th></tr>
          </thead>
          <tbody>
            <tr><td>1</td><td>刘小龙</td><td>88</td></tr>
            <tr><td>2</td><td>张大伟</td><td>66</td></tr>
          </tbody>
        </table>
      `,
    })
  },
  methods: {
    async exportViaServer() {
      const html = this.editor.getHTML();
      console.log('html', html);

      // 内联一些样式,提升导出效果
      const style = `
        <style>
          body{ font-family: 'Arial', sans-serif; font-size:12pt; color:#111; }
          table { border-collapse: collapse; width:100%;}
          th { background: #F9FAFB; font-weight: 600; }
          td, th { border: 1px solid #ccc; }
        </style>
      `;

      const fullHtml = `<html><head><meta charset="utf-8">${style}</head>
                         <body>${html}</body>
                        </html>`;

      try {
        const resp = await fetch('http://localhost:8088/export-docx', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ html: fullHtml, filename: 'tiptap-export.docx' })
        });

        if (!resp.ok) {
          const txt = await resp.text();
          throw new Error('导出失败:' + txt);
        }
        const blob = await resp.blob();
        // 下载
        const url = window.URL.createObjectURL(blob);
        const a = document.createElement('a');
        a.href = url;
        a.download = 'tiptap-export.docx';
        document.body.appendChild(a);
        a.click();
        a.remove();
        window.URL.revokeObjectURL(url);
      } catch (err) {
        console.error(err);
        alert('导出失败:' + err.message);
      }
    }
  },
  beforeDestroy() { this.editor?.destroy() }
}
</script>
<style scoped>
.btn { padding:6px 10px; border-radius:4px; border:1px solid #ddd; background:#fff; cursor:pointer }
.editor-box { min-height: 220px; padding: 12px; background:#fff; }

/* ====== 关键:为编辑器内 table 添加样式 ====== */
/* Vue2 scoped 下使用深度选择器确保样式能命中 ProseMirror 渲染的元素 */
/* 如果你的环境不识别 ::v-deep,请试试 >>> 或 /deep/ */
::v-deep .ProseMirror table {
  width: 100%;
  border-collapse: collapse;
  table-layout: auto;
  margin: 6px 0;
}

/* 单元格边框与内边距 */
::v-deep .ProseMirror th,
::v-deep .ProseMirror td {
  border: 1px solid #d1d5db; /* 灰色边框,可按需调整 #e5e7eb #cbd5e1 等 */
  padding: 8px 10px;
  vertical-align: middle;
}

/* 表头样式(可选) */
::v-deep .ProseMirror th {
  background: #f9fafb;
  font-weight: 600;
}

/* 表格在窄屏时不要换行太多(可选) */
::v-deep .ProseMirror td {
  word-break: break-word;
}

/* 如果使用 Tailwind 的 prose 可能会有默认的 table 样式,下面加个更高权重以覆盖 */
::v-deep .ProseMirror table,
::v-deep .ProseMirror th,
::v-deep .ProseMirror td {
  /* 提高优先级避免被 reset 覆盖 */
  border-color: #d1d5db;
}

/* Tiptap 表格单元格被选中时的高亮效果(与官网一致) */
::v-deep .ProseMirror .selectedCell {
  background-color: rgba(200, 200, 255, 0.4) !important;
  /* 防止 reset 覆盖 */
}

/** 下面是解决列宽拖拽调整时鼠标样式问题的 CSS */
/* 提升优先级:靠右侧 5~8px 时鼠标应变成 col-resize */
::v-deep .ProseMirror table td,
::v-deep .ProseMirror table th {
  position: relative;
}
::v-deep .ProseMirror table td::after,
::v-deep .ProseMirror table th::after {
  content: "";
  position: absolute;
  right: -3px;     /* 命中区域 */
  top: 0;
  width: 6px;
  height: 100%;
  cursor: col-resize !important;
}
</style>

4,运行测试

(1)启动后台服务,访问前端页面,点击“服务端导出为 Word”按钮后,浏览器会自动通过后端生成并下载 Word 文件。

(2)打开下载下来的 docx 文件,可以看到文件内容如下:
评论

全部评论(0)

回到顶部