前言:实现导入 word 并解析成 HTML 的功能,使用的是一个开源的代码库 word-file-transform。但其中也有修改的部分,所以还是把代码放到项目中,而不是使用 npm 下载。
实现功能:
- 增加一个菜单项,使用 element-plus 的 el-upload 组件,上传 docx 文件
- 将 word 文件解析成 HTML ,并且替换整个文档
1、svg小图标
图标可以从 fontawesome 这个网站找,这个网站挺好用,还都提供的有svg图标。
src/icons/import.svg
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="24" height="24">
<path d="M128 64c0-35.3 28.7-64 64-64L352 0l0 128c0 17.7 14.3 32 32 32l128 0 0 288c0 35.3-28.7 64-64 64l-256 0c-35.3 0-64-28.7-64-64l0-112 174.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 39L128 288l0-224zm0 224l0 48L24 336c-13.3 0-24-10.7-24-24s10.7-24 24-24l104 0zM512 128l-128 0L384 0 512 128z"/>
</svg>
2、下载word-file-transform
将其中的 src 目录放到编辑器的 utils 目录下。
其中用到了另一个依赖,需要下载一下 npm install --save mammoth
。并且,引入方式要从 require
改成 import
。在 src/utils/word-file-transform/config/transformFn.js 组件中修改。
其实这个库还用到了另外一个依赖 nanoajax
,是用来上传图片的,但是经过我的测试,用它上传图片总是报错,所以关于上传图片的代码我有一些修改。但是上传图片主要还是要依赖于你的项目中后端提供的接口,所以说,如果没后端接口的话,图片就解析不了了。
主要修改了
src/utils/word-file-transform/config/transformFn.js
import mammoth from "mammoth";
import { defaultConfig } from "../config/config";
import { uploadFile } from "@/utils/uploader";
export default {
"image": async params => {
try {
const {
imageUploadUrl,
imageUrlTransformFn,
imageTransformErrorFn,
imageTransformSuccessFn,
headers = {}
} = params;
return await mammoth.images.imgElement(async image => {
const { imageFilterType, imageTypeErrorMsg } = defaultConfig;
const imageType = image.contentType;
if (imageFilterType.test(imageType)) {
const errObj = imageTypeErrorMsg(imageType);
imageTransformErrorFn && imageTransformErrorFn(errObj);
return false;
}
try {
// 将图片 buffer 转换为 File 对象
const imageBuffer = await image.read("base64");
const byteCharacters = atob(imageBuffer);
const byteArrays = [];
for (let offset = 0; offset < byteCharacters.length; offset += 512) {
const slice = byteCharacters.slice(offset, offset + 512);
const byteNumbers = new Array(slice.length);
for (let i = 0; i < slice.length; i++) {
byteNumbers[i] = slice.charCodeAt(i);
}
const byteArray = new Uint8Array(byteNumbers);
byteArrays.push(byteArray);
}
const blob = new Blob(byteArrays, { type: imageType });
const file = new File([blob], `word-image-${Date.now()}.${imageType.split('/')[1]}`, { type: imageType });
// 使用 uploader.ts 中的方法上传图片
const uploadResult = await uploadFile({
file,
url: imageUploadUrl,
headers,
data: {
// 可选的额外表单数据
type: 'image'
},
onProgress: (progress) => {
console.log('Upload progress:', progress);
}
});
// 处理上传结果
imageTransformSuccessFn && imageTransformSuccessFn(uploadResult);
// 返回图片 URL
const imageUrl = imageUrlTransformFn ? imageUrlTransformFn(uploadResult) : uploadResult;
return { src: imageUrl };
} catch (error) {
imageTransformErrorFn && imageTransformErrorFn({
msg: '图片上传失败:' + (error.message || '未知错误')
});
return false;
}
});
} catch (error) {
return false;
}
},
"html": params => {
const { input, options, config } = params;
mammoth.convertToHtml(input, options)
.then(res => {
config.transformSuccessFn({
content: res.value,
title: config.wordTitle
});
})
.catch(err => {
config.transformErrorFn && config.transformErrorFn(err);
});
}
};
其中上传的方法:
src/utils/uploader.ts
export interface UploadOptions {
file: File
url: string
headers?: Record<string, string>
onProgress?: (progress: number) => void
data?: Record<string, any>
}
export interface UploadResult {
url: string
[key: string]: any
}
export async function uploadFile(options: UploadOptions): Promise<UploadResult> {
const { file, url, headers = {}, onProgress, data = {} } = options
const formData = new FormData()
formData.append('file', file)
// 添加额外的表单数据
Object.entries(data).forEach(([key, value]) => {
formData.append(key, value)
})
try {
const response = await fetch(url, {
method: 'POST',
headers: {
...headers
},
body: formData
})
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return await response.json();
} catch (error) {
throw new Error(`文件上传失败: ${error instanceof Error ? error.message : '未知错误'}`)
}
}
3、上传文件的组件
上传文件使用的是 element-plus 的 el-upload 组件
其中的 beforeUpload 方法,在这个开源库中有示例,在这个函数中获取文件并且进行转换操作。最终返回的是 reject()
,也就是阻止后续的上传操作。在我的代码中,进行了一些修改,主要是在解析成功的回调函数中,去调用命令,重新设置编辑器的内容。其中还使用了 el-loading
组件,在上传的过程中增加一个覆盖框,解析完毕后给出提示
src/components/menu-commands/word-import.upload.vue
<template>
<div>
<el-upload ref="upload" class="word-upload" :show-file-list="false" accept=".docx" :before-upload="beforeUpload">
<command-button :tooltip="t('editor.extensions.WordImport.tooltip')" icon="import" />
</el-upload>
</div>
</template>
<script lang="ts">
import { defineComponent, inject, ref } from 'vue';
import { ElUpload, ElButton, ElLoading, ElMessage } from 'element-plus';
import WordFileTransform from '@/utils/word-file-transform';
import { Editor } from '@tiptap/core';
import CommandButton from './command.button.vue';
export default defineComponent({
components: {
CommandButton,
ElUpload,
ElButton
},
props: {
editor: {
type: Editor,
required: true,
},
buttonIcon: {
default: '',
type: String
},
uploadUrl: {
type: String,
required: true,
},
},
setup() {
const t = inject('t');
const enableTooltip = inject('enableTooltip', true);
const fileInput = ref<HTMLInputElement | null>(null);
const wordHtml = ref<string>('');
const loading = ref(false);
return { t, enableTooltip, fileInput, wordHtml, loading };
},
methods: {
beforeUpload(file, fileList) {
return new Promise(async (resolve, reject) => {
const loadingInstance = ElLoading.service({
lock: true,
text: '正在解析文档...',
background: 'rgba(0, 0, 0, 0.7)',
});
const _this = this;
const transformSuccessFn = function (data) {
try {
_this.editor.commands.setWordContent(data.content);
loadingInstance.close();
ElMessage.success('文档导入成功!');
} catch (error) {
loadingInstance.close();
ElMessage.error('文档处理失败:' + error.message);
}
};
const transformErrorFn = function (err) {
loadingInstance.close();
ElMessage.error('文档解析失败:' + (err?.message || '未知错误'));
};
try {
const userToken = '';
const props = {
file,
config: {
imageTransform: {
imageUploadUrl: this.uploadUrl,
axiosPostConfig: { headers: { token: userToken } }
},
wordTransform: {
transformSuccessFn,
transformErrorFn
}
}
};
let WordTransform = new WordFileTransform(props);
await WordTransform.wordTransform();
} catch (error) {
loadingInstance.close();
ElMessage.error('文档处理出错:' + error.message);
}
reject();
});
},
},
});
</script>
<style>
.word-upload .el-upload {
display: inline-block;
vertical-align: top;
}
</style>
4、扩展文件
扩展文件需要提供修改编辑器内容的方法,其中需要自行设置图片上传地址 uploadUrl
import { Extension } from '@tiptap/core';
import { Editor } from '@tiptap/vue-3';
import WordImportUpload from '@/components/menu-commands/word-import.upload.vue';
const uploadUrl = '你的图片上传的地址';
export interface WordImportOptions {
uploadUrl: string;
}
declare module '@tiptap/core' {
interface Commands<ReturnType> {
wordImport: {
setWordContent: (content: string) => ReturnType;
};
}
interface EditorOptions {
wordImport?: Partial<WordImportOptions>;
}
}
const WordImport = Extension.create<WordImportOptions>({
name: 'wordImport',
addCommands() {
return {
setWordContent:
(content: string) =>
({ commands }) => {
if (content) {
// 替换整个文档,第二个参数表示是否触发更新的监听器
return commands.setContent(content, true);
}
return false;
},
};
},
addOptions() {
return {
button({ editor }: { editor: Editor }) {
return {
component: WordImportUpload,
componentProps: {
editor,
uploadUrl
},
};
},
};
},
});
export default WordImport;
还有其他的文件的修改,例如 tooltip 的设置、扩展在根组件的引入等,就不一一列举了,相信看到这里的童鞋对这些常规修改已经灰常的熟悉了,不熟悉的童鞋可以看看我之前的文章哦。