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

相关推荐
桂月二二4 小时前
探索前端开发中的 Web Vitals —— 提升用户体验的关键技术
前端·ux
hunter2062065 小时前
ubuntu向一个pc主机通过web发送数据,pc端通过工具直接查看收到的数据
linux·前端·ubuntu
qzhqbb5 小时前
web服务器 网站部署的架构
服务器·前端·架构
刻刻帝的海角5 小时前
CSS 颜色
前端·css
浪浪山小白兔6 小时前
HTML5 新表单属性详解
前端·html·html5
lee5767 小时前
npm run dev 时直接打开Chrome浏览器
前端·chrome·npm
2401_897579657 小时前
AI赋能Flutter开发:ScriptEcho助你高效构建跨端应用
前端·人工智能·flutter
limit for me7 小时前
react上增加错误边界 当存在错误时 不会显示白屏
前端·react.js·前端框架
浏览器爱好者7 小时前
如何构建一个简单的React应用?
前端·react.js·前端框架
qq_392794488 小时前
前端缓存策略:强缓存与协商缓存深度剖析
前端·缓存