业务需求
以富文本的形式,实现文本+表格内容的展示,部分内容以变量的形式动态获取,并支持导出为word文档
代码上需要实现的功能:
1.能读取一个固定样式的docx模板,使用从接口获取的内容,替换掉文档中的变量,并保留原样式导出
2.能将读取到的docx模板展示到富文本编辑器中,并且可以编辑替换对应的变量为真实的内容
3.在富文本编辑器中修改内容后,要将修改后的结果,替换掉文档中的变量,并保留原样式导出
实现方案:
1.根据已有的word文档模板,定义好一套富文本形式的模板,需要动态渲染的内容以${xxx}的形式替换
2.从接口查询变量的内容并替换,将该段富文本模板以html的形式展示
3.将替换后的内容填充到富文本编辑器内,以供修改
4.修改完成后,可以导出为word文档,内容要绝对一致,样式要高度一致(允许轻度的偏差)
如图:
CKEditor介绍
基础使用
预定义构建
官网提供了几种内置的模式,这几种模式都是官方打包的好的,称为预定义构建。
这几种模式之间主要是工具栏的展示方式和功能范围不同(这里后面有坑)。
常用的是第一种经典模式,每种模式的安装和引入的包是不一样的。
以vue3为例,以下几步就能生成一个Claasic类型的editor:
typescript
// cmd
npm install --save @ckeditor/ckeditor5-vue @ckeditor/ckeditor5-build-classic
------------------------------------------------------------------------------
// main.js
import CKEditor from '@ckeditor/ckeditor5-vue';
Vue.createApp( { /* options */ } ).use( CKEditor ).mount( /* DOM element */ );
------------------------------------------------------------------------------
// index.vue
<template>
<div id="app">
<ckeditor :editor="editor" v-model="editorData" :config="editorConfig"></ckeditor>
</div>
</template>
<script>
import ClassicEditor from '@ckeditor/ckeditor5-build-classic';
export default {
name: 'app',
data() {
return {
editor: ClassicEditor,
editorData: '<p>Content of the editor.</p>',
editorConfig: {
// The configuration of the editor.
}
};
}
}
</script>
结果:
生成以后,发现没有我们需要导出word 功能,并且也没有基础的居中 ,字号,字体,颜色等功能也没有,起初以为是小问题,加几个属性参数就有了
于是按照官方文档的参数配置加了一遍,发现一点效果都没有。
经过调查才发现,这些功能都是需要下载插件才能使用的,预定义构建内置的功能只支持一部分。
我们需要的导出功能都是不内置的。以下是不同类型能支持的工具栏。
也就是说,我有的你可以不用,但我没有的你一定用不了。
预定义的编辑器所支持的功能如下:
虽然最后一种Superbuild类型的内置构建能提供我们需要的功能,但只支持CDN的形式,并且包含大量代码。项目中可能不需要所有的。
官网也说最好将超级构建用于测试和评估目的,而不是在生产环境中使用。
对于生产环境中的自定义和高效解决方案,强烈建议使用 在线构建器方法 或 从源代码构建编辑器。
自定义构建
预定义的编辑器不满足我们的需求,就只能使用自定义构建的方式了,自定义构建有两种方式:
1.使用在线构建器创建自定义构建
2.从源代码构建
1.在线构建器构建
具体教程可以查看:Vue3中快速简单使用CKEditor 5富文本编辑器
简单总结就是:通过官方提供的图形化配置界面,选择自己合适的编辑器类型和需要的功能,生成代码文件,然后使用npm安装本地包的形式,引入到自己项目中,然后后续的用法就和预定义构建是一样的。
效果预览:
注意:导出功能等功能需要收费(大约80刀/月),且需要能访问外网。如果未验证身份导出的内容有水印,并且需要付费后使用云服务功能,似乎不能在我们的内网环境使用。
注册步骤:
-
注册和订阅:在CKEditor官网上注册一个账户,并选择适合你需求的订阅计划。确保你的订阅套餐包含CKEditor Cloud Services的导出功能。
-
获取API密钥:成功注册并订阅后,CKEditor将为你生成一个独特的API密钥。这个API密钥将用于验证和授权你的应用程序访问CKEditor Cloud Services。
-
集成到应用程序中:按照CKEditor提供的文档将CKEditor集成到你的应用程序中。确保正确引入CKEditor的资源文件,并设置好相应的配置参数。
-
配置云服务:在CKEditor初始化代码中,添加云服务的配置信息,包括API密钥和其他必要参数。
有一种免费思路:使用CKEditor自带的.getData()方法,可以将编辑器内容读取为html文档,所以我们直接将html文档转化为word再导出即可,需要使用到htmlDocx和saveAs两个库读取html并导出为word
而且在线自定义构建的包下载导入后,内部有一些奇怪的报错。
2.从源码构建
鉴于在线构建器的包会有各种报错,也没找到原因。最终我们使用了从源码构建的方式。
有几个需要注意的点:
-
因为官方导出收费且不支持内网,所以我们采用自定义导出的方式
-
一些通过工具栏修改后的样式,导出成word后并不兼容,需要手动转成可识别的样式,比如:hsl的颜色改成16进制或RGBA类型的
-
每一个工具栏项都需要手动引入对应的包并配置
这里直接展示示例:
javascript
<script setup>
import { onMounted, reactive, ref } from "vue";
// import ClassicEditor from "@ckeditor/ckeditor5-build-classic";
// import * as Editor from "ckeditor-custom/build/ckeditor";
// import DecoupledEditor from "@ckeditor/ckeditor5-build-decoupled-document";
import { DecoupledEditor } from "@ckeditor/ckeditor5-editor-decoupled";
// 中文语言包
import "@ckeditor/ckeditor5-build-decoupled-document/build/translations/zh-cn.js";
import { saveAs } from "file-saver";
import { getEditorConfig } from "./utils";
const editor = ref(DecoupledEditor);
const editorData = ref("");
const originData =
'<p style="text-align:center;"><span style="background-color:#ffff00;font-size:21px;"><strong>【${title}】</strong></span></p><p style="text-align:center;">${subTitle}</p><p> </p><p> </p><p style="text-align:center;"> </p><p style="text-align:center;"> </p><p style="text-align:center;"> </p><p style="text-align:center;"> </p><p style="text-align:center;"> </p><p style="text-align:center;"> </p><p style="text-align:center;">资产管理人: ${company}</p><p style="text-align:center;">资产托管人: <span style="background-color:#ffff00;">【${fullName}】</span></p>';
const getModelHtml = (mhtml, style) => {
return `
<!DOCTYPE html>
<html>
<head>
<style>
table {
display: table;
border: 1px double rgb(179, 179, 179);
border-collapse: collapse;
border-spacing: 0px;
width: 100%;
height: 100%;
}
thead th {
background-color: #ebebeb;
}
</style>
</head>
<body>
${mhtml}
</body>
</html>
`;
};
// const editorIns = ref()
const getHtml = () => {
let data = editorIns.value.getData();
console.log(data, "data====");
data = data.replace(/figure/g, "div");
// data = data.replace("<table", "<table cellspacing='-1' ");
return data;
};
const exportWord = () => {
let html = getModelHtml(getHtml());
console.log(html, "html");
// 使用我们刚刚准备好的html模板并创建Blob对象
let blob = new Blob([html], { type: "application/msword;charset=utf-8" });
// 调用FileSaver.saveAs导出下载word
saveAs(blob, "template.docx");
};
const fetchData = () => {
// 模拟接口获取的字段
let data = originData;
const wordData = {
title: "文档标题",
name: "兴全基金",
};
data = data.replace("${title}", "产品全称");
data = data.replace("${subTitle}", "清算报告");
data = data.replace("${company}", "兴证全球基金管理有限公司");
data = data.replace("${fullName}", "托管行全称");
console.log(data, "data");
editorData.value = data;
};
const editorIns = ref(null);
const onReady = (editor) => {
editorIns.value = editor;
// editorConfig.value.plugins.push(customPlugin(editorIns.value));
console.log(editor.getData(), "editor.getData()");
editor.ui
.getEditableElement()
.parentElement.insertBefore(editor.ui.view.toolbar.element, editor.ui.getEditableElement());
};
onMounted(() => {
fetchData();
});
</script>
<template>
<div class="root">
<div class="temp">
<p>模板:</p>
<div v-html="originData"></div>
</div>
<div class="editor-container">
<div class="btn" @click="exportWord">导出</div>
<ckeditor :editor="editor" v-model="editorData" :config="getEditorConfig()" @ready="onReady"></ckeditor>
</div>
<!-- <div id="editor"></div> -->
<!-- <CKEditor :editor="state.editor" v-model="state.editorData" :config="state.editorConfig"></CKEditor> -->
</div>
</template>
<style>
.root {
padding: 20px;
}
.editor-container {
position: relative;
}
/* 展示的表格样式 */
figure {
width: 100%;
height: 100%;
}
.ck-content {
border: 1px solid #ccc !important;
border-top: none !important;
}
.temp {
border: 1px solid #ccc;
margin-bottom: 20px;
}
.btn {
border: 1px solid #ccc;
width: fit-content;
cursor: pointer;
position: absolute;
right: 5px;
top: 6px;
}
</style>
import { Paragraph } from "@ckeditor/ckeditor5-paragraph";
import { Essentials } from "@ckeditor/ckeditor5-essentials";
import { Bold, Italic, Strikethrough, Underline, Subscript, Superscript, Code } from "@ckeditor/ckeditor5-basic-styles";
import { FontColor, FontSize, FontBackgroundColor } from "@ckeditor/ckeditor5-font";
import { Table, TableToolbar, TableProperties, TableCellProperties } from "@ckeditor/ckeditor5-table";
import { Alignment } from "@ckeditor/ckeditor5-alignment";
import { Heading } from "@ckeditor/ckeditor5-heading";
import { TextTransformation } from "@ckeditor/ckeditor5-typing";
import {
Image,
ImageInsert,
ImageCaption,
ImageResize,
ImageStyle,
ImageToolbar,
ImageUpload,
} from "@ckeditor/ckeditor5-image";
const colorOptions = [
{
color: "#000000", // 黑色
label: "Black",
},
{
color: "#ff0000", // 红色
label: "Red",
},
{
color: "#A020F0", // 紫色
label: "Purple",
},
{
color: "#ffa500", // 橘黄色
label: "Orange",
},
{
color: "#ffff00", // 黄色
label: "yellow",
},
];
export const getEditorConfig = () => {
return {
language: "zh-cn",
plugins: [
Heading, // 标题功能
Paragraph, // 段落
Essentials,
TextTransformation, // 文本转换
Bold, // 加粗
Italic, // 斜体
Strikethrough, // 删除线
Underline, // 上划线
Subscript, // 下标
Superscript, // 上标
Code, // 行内代码
FontColor, // 字体颜色
FontSize, // 字体大小
FontBackgroundColor, // 字体背景颜色
Alignment, // 对齐
Table, // 表格
TableToolbar, // b
// TableProperties,
// TableCellProperties,
// Image,
// ImageToolbar,
// ImageCaption,
// ImageStyle,
// ImageResize,
// ImageInsert,
// LinkImage,
// ImageUpload,
],
toolbar: [
"Heading",
"undo", // 撤销
"redo", // 重做
"selectAll", // 全选
"|",
// "Bold",
"Italic",
// "Strikethrough",
// "Underline",
// "Subscript",
// "Superscript",
// "Code",
"FontColor", // 字体颜色
"FontSize", // 字体大小
"FontBackgroundColor", // 字体背景色
"Alignment", // 对齐
"|",
"insertTable", // 表格
// "ImageInsert",
// "imageUpload",
// "ImageToolbar",
// "ImageCaption",
// "imageStyle:side",
// "|",
// "toggleImageCaption",
// "imageTextAlternative",
// "|",
// "linkImage",
],
table: {
contentToolbar: ["tableColumn", "tableRow", "mergeTableCells", "tableProperties", "tableCellProperties"],
tableProperties: {
// Configuration of the TableProperties plugin.
// ...
borderColors: colorOptions,
backgroundColors: colorOptions,
},
tableCellProperties: {
borderColors: colorOptions,
backgroundColors: colorOptions,
},
},
fontSize: {
options: [9, 11, 13, 15, 17, 19, 21],
},
fontColor: {
colors: colorOptions,
},
fontBackgroundColor: {
colors: colorOptions,
},
typing: {
transformations: {
include: [{ from: "${CODE}", to: "123" }],
},
},
};
};
可以将一些固定的标准样式,提前写入style中,保证导出后的文档样式和编辑器内显示的样式差距不会太大。