当前位置: 首页 > article >正文

【element-tiptap】导出word

前言:前面的文章 【element-tiptap】导入word并解析成HTML 已经介绍过如何在 element-tiptap 中导入 word。这篇文章来探究一下怎么将编辑器的内容导出成word

(一)创建菜单项

1、图标

首先上 fontawesome 这个网站上找一个合适的图标,把它的 svg 复制下来
然后创建 src/icons/export.svg,增加 widthheightfill 属性

<svg xmlns="http://www.w3.org/2000/svg" width="28" height="32" viewBox="0 0 576 512">
  <path fill="currentColor" d="M0 64C0 28.7 28.7 0 64 0L224 0l0 128c0 17.7 14.3 32 32 32l128 0 0 128-168 0c-13.3 0-24 10.7-24 24s10.7 24 24 24l168 0 0 112c0 35.3-28.7 64-64 64L64 512c-35.3 0-64-28.7-64-64L0 64zM384 336l0-48 110.1 0-39-39c-9.4-9.4-9.4-24.6 0-33.9s24.6-9.4 33.9 0l80 80c9.4 9.4 9.4 24.6 0 33.9l-80 80c-9.4 9.4-24.6 9.4-33.9 0s-9.4-24.6 0-33.9l39-39L384 336zm0-208l-128 0L256 0 384 128z"/>
</svg>
2、创建扩展

src/extensions/export.ts

import { Extension } from '@tiptap/core';
import { Editor } from '@tiptap/vue-3';
import ExportDropdown from '@/components/menu-commands/export.dropdown.vue';

export interface ExportOptions {
}

declare module '@tiptap/core' {
  interface Commands<ReturnType> {
    export: {
      exportToWord: () => ReturnType;
    };
  }
}

const Export = Extension.create<ExportOptions>({
  name: 'export',

  addCommands() {
    return {
      exportToWord:
        () =>
        ({ editor }) => {
          // 这里可以添加导出 Word 的具体实现
          return true;
        },
      exportToPdf:
        () =>
        ({ editor }) => {
          // 这里可以添加导出 PDF 的具体实现
          return true;
        },
    };
  },

  addOptions() {
      return {
          button({ editor, t }: { editor: Editor; t: (...args: any[]) => string }) {
              return {
                  component: CommandButton,
                  componentProps: {
                      command: () => {
                          editor.commands.exportToWord();
                      },
                      isActive: editor.isActive('export'),
                      icon: 'export',
                      tooltip: t('editor.extensions.Export.tooltip'),
                  },
              };
          },
      };
  },
});

export default Export; 
2、在根组件中引入当前的扩展

老生常谈的步骤,这里就不讲了啦
以及定义菜单的提示语
src/i18n/locales/zh/index.ts

Export: {
  tooltip: '导出文档',
},

在这里插入图片描述

(二)实现导出 word

使用开源库 prosemirror-docx。

1、安装依赖
npm install prosemirror-docx --save

后来,我发现源码里还是有很多不完善的地方,官网还有处于 open 状态的提交记录😂
在这里插入图片描述
其中的 #35 还是很关键的,是修复的图片尺寸检测失败的 bug。所以还是将源码中的 src 文件夹都放在自己的项目里面
在这里插入图片描述
另外还需要安装一些别的依赖,直接使用 npm install xxx 安装即可

"docx": "^9.0.3",
"file-saver": "^2.0.5",
"image-dimensions": "^2.3.0",
2、图片 buffer

根据 export-tiptap-content-to-microsoft-word 上给出的示例代码,

import {
  writeDocx,
  DocxSerializer,
  defaultNodes,
  defaultMarks
} from "prosemirror-docx";
import { saveAs } from "file-saver";

const nodeSerializer = {
  ...defaultNodes,
  hardBreak: defaultNodes.hard_break,
  codeBlock: defaultNodes.code_block,
  orderedList: defaultNodes.ordered_list,
  listItem: defaultNodes.list_item,
  bulletList: defaultNodes.bullet_list,
  horizontalRule: defaultNodes.horizontal_rule,
  image(state, node) {
    // No image
    state.renderInline(node);
    state.closeBlock(node);
  }
};

const docxSerializer = new DocxSerializer(nodeSerializer, defaultMarks);

const write = useCallback(async () => {
   const opts = {
     getImageBuffer(src) {
       return Buffer.from("Real buffer here");
     }
   };

   const wordDocument = docxSerializer.serialize(editor.state.doc, opts);

   await writeDocx(wordDocument, (buffer) => {
     saveAs(new Blob([buffer]), "example.docx");
   });
 }, [editor?.state.doc]);

使用这个库需要将图片转成 buffer
使用 Buffer.from() 方法,需要传递一个 base64 字符串。当前的图片,是保存在服务器返回的地址上,需要转换成 base64。起先我想在 getImageBuffer 方法中异步加载远程的图片资源,然后转成 base64,后来发现这个方法只能是同步的,所以就需要先遍历文档所有节点,将所有的图片都转成 base64。

addCommands() {
    return {
        exportToWord:
            () =>
                async ({ editor }) => {
                    // 给编辑器中所有的图片的src转换为base64

                    const images: Node[] = [];
                    editor.state.doc.descendants((node) => {
                        if (node.type.name === 'image') {
                            images.push(node);
                        }
                    });

                    for (const image of images) {
                        const src = image.attrs.src;
                        if (!src.startsWith('data:')) {
                            try {
                                const response = await fetch(src);
                                const blob = await response.blob();
                                const reader = new FileReader();
                                const base64 = await new Promise<string>((resolve) => {
                                    reader.onload = () => resolve(reader.result as string);
                                    reader.readAsDataURL(blob);
                                });

                                const tr = editor.state.tr;
                                const pos = editor.state.doc.resolve(0);
                                editor.state.doc.nodesBetween(0, editor.state.doc.content.size, (node, pos) => {
                                    if (node.type.name === 'image' && node.attrs.src === src) {
                                        tr.setNodeMarkup(pos, undefined, {
                                            ...node.attrs,
                                            src: base64
                                        });
                                    }
                                });
                                editor.view.dispatch(tr);
                            } catch (error) {
                                console.error('Failed to convert image to base64:', error);
                            }
                        }
                    }


                    const opts = {
                        getImageBuffer(src) {
                            // base64转 Buffer
                            return Buffer.from(src.split(',')[1], 'base64');
                        }
                    };

                    const wordDocument = docxSerializer.serialize(editor.state.doc, opts);
                    await writeDocx(wordDocument, (buffer) => {
                        saveAs(new Blob([buffer]), "example.docx");
                    });


                    return true;
                },
    };
},
3、源码bug修复

使用这个代码出现的一些问题:
1、图片尺寸检测不出来,需要根据 这个提交 去修改代码
2、图片会变得很大,是因为图片的宽高没有传进去,使用的是默认的宽高
3、图片默认居中,对齐方式显示成 left 即可
src/utils/prosemirror-docx/schema.ts

// Presentational
image(state, node) {
  const { src, width, height } = node.attrs;
  state.image(src, width, height);
  state.closeBlock(node);
},

src/utils/prosemirror-docx/serializer.ts

image(
  src: string,
  width: number,
  height: number,
  widthPercent = 70,
  align: AlignOptions = 'left',
  imageRunOpts?: IImageOptions,
) {
  const buffer = this.options.getImageBuffer(src);

  const dimensions = imageDimensionsFromData(buffer);
  if (!dimensions) return;
  const aspect = dimensions.height / dimensions.width;
  if(!width) {
    width = this.maxImageWidth * (widthPercent / 100);
  }

  this.current.push(
    new ImageRun({
      data: buffer,
      transformation: {
        width,
        height: height || width * aspect,
      },
      ...imageRunOpts,
    }),
  );

  return this.addParagraphOptions({
    alignment: align,
  });
}

以上就可以实现导出成 word 的功能啦!
看下效果:
文档如下:
在这里插入图片描述
导出word如下:
在这里插入图片描述

(三)todoList、文本颜色、背景颜色处理

经过实际验证,文档中的todoList,docx 库是解析不了的,控制台会报错
在这里插入图片描述
需要将它处理成 [] xxx 的形式。
另外,背景颜色和文字颜色没有带到 word 中
src/utils/prosemirror-docx/serializer.ts

export class DocxSerializer {
  nodes: NodeSerializer;

  marks: MarkSerializer;

  constructor(nodes: NodeSerializer, marks: MarkSerializer) {
    this.nodes = {
      taskList: (state, node) => {
        node.forEach((child, _, i) => {
          state.render(child, node, i);
        });
      },
      taskItem: (state, node) => {
        state.current = [];
        const checkbox = node.attrs.checked ? '[x] ' : '[ ] ';
        state.text(checkbox);
        state.renderContent(node);
        state.closeBlock(node);
      },
      ...nodes,

    this.marks = {
      ...marks,
      textStyle: (state, node, mark) => ({
        color: mark.attrs.color,
      }),
      highlight: (state, node, mark) => ({
        shading: { fill: mark.attrs.color },
      }),
    };
  }
}

http://www.kler.cn/a/418750.html

相关文章:

  • Javascript中DOM事件监听 (鼠标事件,键盘事件,表单事件)
  • Zephyr 入门-设备树与设备驱动模型
  • STL算法之其他算法_上
  • 算法刷题Day5: BM52 数组中只出现一次的两个数字
  • 第144场双周赛:移除石头游戏、两个字符串得切换距离、零数组变换 Ⅲ、最多可收集的水果数目
  • spring-boot-maven-plugin 标红
  • 在CentOS 7上设置Apache的mod_rewrite的方法
  • 【数据结构计数排序】计数排序
  • Java面经之JVM
  • Qt Sensors 传感器控制介绍篇
  • 【JAVA】IntelliJ IDEA 如何创建一个 Java 项目
  • Vue3+node.js实现注册
  • (免费送源码)计算机毕业设计原创定制:Apache+JSP+Ajax+Springboot+MySQL Springboot自习室在线预约系统
  • VPN连不上学校服务器
  • 大模型开发中LCEL与LLMChain响应度的对比
  • Electron PC桌面应用exe开发
  • C#中switch语句使用
  • 大模型日报 2024-12-01
  • 大模型开发和微调工具Llama-Factory-->数据处理
  • Linux设置开启启动脚本
  • Vue 3 服务端渲染(SSR)教程
  • SpringMVC |(一)SpringMVC概述
  • DevOps工程技术价值流:Jenkins驱动的持续集成与交付实践
  • 【青牛科技】电动工具调速控制电路芯片GS016,电源电压范围宽、功耗小、抗干扰能力强
  • Transformers在计算机视觉领域中的应用【第1篇:ViT——Transformer杀入CV界之开山之作】
  • 2.vue3+openlayers加载OpenStreetMap地图