CKEditor使用调研

业务需求

以富文本的形式,实现文本+表格内容的展示,部分内容以变量的形式动态获取,并支持导出为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刀/月),且需要能访问外网。如果未验证身份导出的内容有水印,并且需要付费后使用云服务功能,似乎不能在我们的内网环境使用。

注册步骤:

  1. 注册和订阅:在CKEditor官网上注册一个账户,并选择适合你需求的订阅计划。确保你的订阅套餐包含CKEditor Cloud Services的导出功能。

  2. 获取API密钥:成功注册并订阅后,CKEditor将为你生成一个独特的API密钥。这个API密钥将用于验证和授权你的应用程序访问CKEditor Cloud Services。

  3. 集成到应用程序中:按照CKEditor提供的文档将CKEditor集成到你的应用程序中。确保正确引入CKEditor的资源文件,并设置好相应的配置参数。

  4. 配置云服务:在CKEditor初始化代码中,添加云服务的配置信息,包括API密钥和其他必要参数。

有一种免费思路:使用CKEditor自带的.getData()方法,可以将编辑器内容读取为html文档,所以我们直接将html文档转化为word再导出即可,需要使用到htmlDocx和saveAs两个库读取html并导出为word

而且在线自定义构建的包下载导入后,内部有一些奇怪的报错。

2.从源码构建

鉴于在线构建器的包会有各种报错,也没找到原因。最终我们使用了从源码构建的方式。

有几个需要注意的点:

  1. 因为官方导出收费且不支持内网,所以我们采用自定义导出的方式

  2. 一些通过工具栏修改后的样式,导出成word后并不兼容,需要手动转成可识别的样式,比如:hsl的颜色改成16进制或RGBA类型的

  3. 每一个工具栏项都需要手动引入对应的包并配置

这里直接展示示例:

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中,保证导出后的文档样式和编辑器内显示的样式差距不会太大。

相关推荐
裁二尺秋风36 分钟前
Nginx — Nginx处理Web请求机制解析
前端·nginx
excel40 分钟前
webpack 核心编译器 第五节
前端
曲辒净2 小时前
vue搭建一个树形菜单项目
前端·javascript·vue.js
喝拿铁写前端7 小时前
前端与 AI 结合的 10 个可能路径图谱
前端·人工智能
codingandsleeping7 小时前
浏览器的缓存机制
前端·后端
灵感__idea9 小时前
JavaScript高级程序设计(第5版):扎实的基本功是唯一捷径
前端·javascript·程序员
摇滚侠9 小时前
Vue3 其它API toRow和markRow
前端·javascript
難釋懷9 小时前
JavaScript基础-history 对象
开发语言·前端·javascript
beibeibeiooo9 小时前
【CSS3】04-标准流 + 浮动 + flex布局
前端·html·css3