工作记录:举步维艰的在线 word 之旅 - tinymce

news2024/12/26 11:10:17

项目中需要实现 “在线编辑 word 模板” 的功能,我打算使用富文本组件 tinymce ,因为业务需求比较特殊,研究一下 tinymce 是否能实现。

如何在 vue 项目中引用 tinymce,可以看另一篇文章 《在 vue 项目中使用 tinymce》

(最后这个功能没有用 tinymce 实现,换了别的富文本库。但还是把这次的研究过程记录下来,给自己做一个总结回顾)


业务描述

使用场景

假设我需要写一个 word 文件:《2023年销售部工作汇报》

在这里插入图片描述

写完之后,我还要给开发部、采购部写同样的报告,格式相同,只有金额数值需要根据各部门实际情况填写。写完了今年的报告,我还要补上之前年份的报告。

这些都是重复的工作,我想省点力气。做一个模板,让程序给我自动生成多份 word 文件。

简单观察就能发现,这篇报告中只有两个变量:年度部门。收入支出的值都可以按照设定好的规则,根据这两个变量的值自动计算查询:

在这里插入图片描述
我使用这个模板时,只需要指明 年度部门 的值,程序就会自动把所有值替换好,生成 word 文件。年度和部门是直接替换文字,金额是根据年度和部门的值自动查询数据库后再替换。

上面是简单的描述,真实项目要复杂得多,这里不详细说明了。

解释

  • 年度、部门是 “全局参数”,导出 word 时用户可以设置这个值或者使用默认值(比如说年度默认取当前年)。
  • 总收入、总支出、净收入是“数据标签”,每个标签都需要配置好规则,这样导出 word 时就可以根据全局参数的值实时查询数据

数据标签的规则设置非常灵活,可配置项很多,大部分的业务场景都可以覆盖。这里不细说了,举个例子简单理解一下就够了:(很久不写sql了,不确定语法对不对,明白意思就行)

# [总收入]的规则:
SELECT SUM(income) FROM table_money WHERE year = 年度 and dept = 部门;

除了简单的数据标签,还可以插入多种类型的图表。这些内容也需要配置数据规则、设置想要的样式。导出 word 时查询完数据,会自动渲染:

在这里插入图片描述


用 tinymce 怎么实现

每个数据标签是一个整体

全局参数、数据标签等业务组件,它们其实都是占位符。在编辑模板的时候,这些占位符的名字是由配置决定的(会根据你的配置信息自动给标签起名字),不应该让用户在富文本中直接修改文字。而且每个标签应该是一个整体。

如下图:光标在数据标签内部了,这是不可以的,应该禁止光标点进去:

在这里插入图片描述

光标在净收入标签的后面。这时候用户点击 Backspace 键,应该把 [净收入] 标签整个删除:

在这里插入图片描述
这种效果其实在 CodeMirror 中可以实现,也很好实现,因为 CodeMirror 提供了支持。但 tinymce 中并没提供支持(其实大部分富文本组件都不支持,这对于富文本属于进阶功能了,普通使用者用不到)。我也试了很久,在 tinymce 中想要实现很麻烦,

灵机一动,我可以用 button 标签啊!最终全局参数、数据标签决定用:

<input type="button" value="[年度]">

PROBLEM SOLVED!

(后来在网上看到有人说,Web component 配合 tinymce custom_elements 属性也可以实现类似功能。就不去试了,这里提一下。而且Web component 也有兼容性问题,现在项目还没放弃兼容 IE,用不了)


编辑器中怎么显示图表

项目中的图表很多是用 echarts 实现的,比如柱状、饼图。但 echarts 图是不能放在 tinymce 编辑框中直接用的。所以我使用图片当占位符。
在屏幕外获取数据、渲染图表。渲染好之后,把图表转为图片,再更新占位符的 src。
因为最终导出到 word 后,用户看到的也是一张静态的图片(word里也放不了活的 echarts 图表哇)

给数据标签添加事件

编辑模板时,点击数据标签,要弹出配置弹窗。所以要给数据标签添加点击事件。

tinymce 所有的 set 方法只接受 string 类型的参数。所以不能把 element 直接传给 insertContent,只能传 element.outerHTML。这样绑定的事件就无效了。

onMounted(() => {
  tinymce.init({
    selector: "#target",
    toolbar: "addDataTag",
    setup: (editor) => {
      editor.ui.registry.addButton("addDataTag", {
        text: "插入数据标签",
        onAction: () => {
          const button = editor.getDoc().createElement("input"); 
          button.setAttribute("id", "dataTag1");
          button.setAttribute("type", "button");
          button.setAttribute("value", "数据标签1");          
          button.addEventListener("click", function () { console.log("click");}); // 没用
          // insertContent 方法只接受 string 类型的参数,所以这里只能传 button.outerHTML。绑定的事件函数丢失了。
          editor.insertContent(button.outerHTML);	
        },
      });
    },
  });
});

要想加点击事件,只能先用 tinymce set。等页面中已经有这个元素后,再去找到它并绑定事件:

        onAction: () => {
          ...
          // 绑不上事件!
          button.addEventListener("click", function () { console.log("before");	});
          editor.insertContent(button.outerHTML);
          const res = editor.getDoc().querySelector("#dataTag1");
          // 成功绑定事件!
          res.addEventListener("click", function () { console.log("after"); });	
        },

但是这样绑定的事件是临时的:每次页面回显数据时,都要给数据标签重新绑定一遍事件。

onMounted(() => {
  tinymce.init({...});
  setTimeout(() => {
    // 从服务器获取数据并回显
    tinymce.activeEditor.setContent(`<input id="dataTag1" type="button" value="数据标签1">`);
    // 绑定事件
    const widgets = tinymce.activeEditor.getDoc().querySelector("input[type='button']");
    widgets.addEventListener("click", function () { console.log("回显时绑定事件"); });
  }, 500);
});

回显后给已有的标签绑定事件,新增时给当前新增的这个绑定事件。很麻烦,不如 每次初始化时,给 editor.getDoc().body 绑定事件,根据 e.srcElement 判断触发元素

(这里只是根据麻烦程度来考虑。但其实给元素绑定事件是根本不可行的,只能给 body 绑,原因在下面会说明)

关联数据

模板内容是一段 html 代码。业务组件的配置项是 object 对象。

要把 html 中代表业务组件的 Element 和配置项关联起来,就需要在 Element 上绑定属性:

<input type="button" value="[年度]" biz-type="globalParams" biz-id="1">

自定义属性和事件被 cleanup 了

但是,我发现 tinymce 在每次获取、设置数据时,都会先执行 cleanup: 清除元素上的自定义属性、事件处理函数等。也就是说,上面的按钮在添加到文档中时, biz-typebiz-id 这两个自定义属性就已经丢失了。

显式调用 tinymce getContent setContent insertContent 方法时会被 cleanup。其他隐式触发 tinymce set 的操作也会悄悄执行 cleanup,防不胜防。举例:

我插入了一个数据标签,点击事件有,一切正常。光标的位置和数据标签在同一行,点击工具栏:插入水平线

在这里插入图片描述

变成:

在这里插入图片描述
再点击”数据标签1“,没有反应,说明绑定的事件已经丢了。我认为原因是:插入水平线功能的内部实现是先 get 了当前行,进行数据处理、计算,再把算好的结果 set 进去,取代以前的内容。所以这一行的内容都被替换了,只是看上去一样罢了。

我也验证了这个想法:用浏览器的调试工具,把之前的 element 存下来了。和之后的 element 进行比较,果然是不相等的:

在这里插入图片描述

前后已经不是同一个元素了:

在这里插入图片描述
这也说明,在上一部分 “给数据标签添加事件” 中,给具体元素绑定事件是不可行的,很容易就会因为 cleanup 而丢失。只能采用给整个 editor.body 绑定事件的做法。

丢失事件的问题用 “给 body 整体绑事件” 就解决了,下面该说丢失自定义属性的问题了。

我原本想看 tinymce 的源码,看看它 cleanup 时用的什么规则,但找了挺久没找到。我决定换个思路,想到 tinymce 自动清理应该是为了规范数据,所以它不认 ”乱七八糟“ 的自定义属性,那正规的属性他就应该认了吧。我试了试 HTML5 提供的自定义属性语法 :

<input type="button" value="[年度]" data-biz-type="globalParams" data-biz-id="1">

成功,tinymce 接受这两个属性了!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/401840.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

HTML文档的基本结构

HTML文档以.html结尾&#xff0c;可以直接使用笔记本创建。 外部结构 <!DOCTYPE HTML><html></html>HTML文档外部结构由DOCTYPE和HTML构成。DOCTYPE告诉浏览器处理的是HTML文档&#xff0c;HTML规定使用HTML5标准。 然后是html元素&#xff0c;他告诉浏览…

基于Spring事件驱动模式实现业务解耦

事件驱动模式 举个例子&#x1f330; 大部分软件或者APP都有会有会员系统&#xff0c;当我们注册为会员时&#xff0c;商家一般会把我们拉入会员群、给我们发优惠券、推送欢迎语什么的。 值得注意的是&#xff1a; 注册成功后才会产生后面的这些动作&#xff1b;注册成功后的…

Flutter(五)容器类组件

布局类组件包含多个子组件&#xff0c;而容器类组件只包含一个子组件 目录填充&#xff08;Padding&#xff09;装饰容器&#xff08;DecoratedBox&#xff09;变换&#xff08;Transform&#xff09;Transform.translate 平移Transform.rotate 旋转Transform.scale 缩放Rotate…

C++中拷贝构造和赋值重载的注意事项以及编译器的优化处理

C中拷贝构造和赋值重载的注意事项以及编译器的优化处理前言1. 拷贝构造和赋值重载的易混淆点和注意事项1.1 易混淆点1.2 注意事项2.编译器对拷贝构造和赋值重载的优化处理前言 本文可以帮助你对下面&#xff1a; &#xff08;1&#xff09;何时调用拷贝构造何时调用赋值重载 &a…

多元统计分析、混合效应模型、结构方程模型、极值统计学、贝叶斯网络、copula

生态环境视角下的多元统计分析 1、多元数据分析:概念、定义、及应用困惑; 2、生态环境数据多元统计方法及应用情景; 3、生态环境多元数据分析预处理; 时长&#xff1a;2小时24分钟 结构方程模型&#xff08;SEM&#xff09;原理、构建流程及应用 1、结构方程模型基本原理 …

Windows 事件日志分析管理

Windows 设备是大多数商业网络中最受欢迎的选择。为了处理这些设备生成的数 TB 的事件日志数据&#xff0c;安全管理员需要使用功能强大的日志管理工具&#xff08;如EventLog Analyzer&#xff09;&#xff0c;该工具可以通过自动执行日志收集、解析、分析、关联和存档等过程来…

STM32—IIC

IIC协议概述 IIC全称Inter-Integrated Circuit (集成电路总线) 是由PHILIPS公司在80年代开发的两线式串行总线&#xff0c;用于连接微控制器及其外围设备。IIC属于半双 工同步通信方式 特点 简单性和有效性。 由于接口直接在组件之上&#xff0c;因此IIC总线占用的空间非常小&…

信捷 XDH Ethercat A_WRITE指令

本指令修改指令轴的当前位置。 什么时候需要用本指令呢&#xff1f;换句话说&#xff0c;用本指令后&#xff0c;坐标原点修改了偏移了。如果在回原点后&#xff0c;往前走了一段距离x,如果是用绝对模式执行把位置修改成0&#xff0c;那么下一次开始每次做绝对运动A_MOVEA&…

黑客为什么戴盖伊福克斯面具

本文是是番外篇&#xff0c;是作者闲的没事把昼夜写完之后瞎写的文章 文章目录前言一、盖伊福克斯是谁&#xff1f;人物背景二、原因匿名面具背后的故事是什么&#xff1f;总结前言 在网上搜索黑客应该是戴帽子的或者是戴面具的 黑客在常人眼中是无所不能的可以上天入地&#…

vue-cropper 拖动图片和截图框

现象 开发遇到vue--cropper不能拖动图片和截图框 解决方法 can-move-box设置为true&#xff0c;表示可以拖动截图框 can-move设置为true&#xff0c;表示可以拖动图片 *注意&#xff1a; 我外层套了一个el-col, el-col的宽高一定要大于截图框的宽高&#xff0c;否则移动不了…

优化改进YOLOv5算法之添加GIoU、DIoU、CIoU、EIoU、Wise-IoU模块(超详细)

目录 1、IoU 1.1 什么是IOU 1.2 IOU代码 2、GIOU 2.1 为什么提出GIOU 2.2 GIoU代码 3 DIoU 3.1 为什么提出DIOU 3.2 DIOU代码 4 CIOU 4.1 为什么提出CIOU 4.2 CIOU代码 5 EIOU 5.1 为什么提出EIOU 5.2 EIOU代码 6 Wise-IoU 7 YOLOv5中添加GIoU、DIoU、CIoU、…

等离子纳秒高压脉冲电源维修HVP-20 P

等离子纳秒高压脉冲电源维修HVP-20 P;HVP-10B;HVP-05;HVP-02等型号均可维修 HVP-20 P(N)用于气体放电与低温等离子体的高性能纳秒高压脉冲电源。 HVP-20P(N)采用专有的marx电路&#xff0c;实现高压脉冲电源参数的便捷可调&#xff0c;包括峰值电压0 – 20 KV &#xff08;-2…

Go语言容器之map、list和nil

一、map map和C中map一样&#xff0c;里面存放的是key-value键值对在Go中map是引用类型&#xff0c;声明语法&#xff1a;var map变量名 map[key的类型]value的类型package mainimport "fmt"func main() {var mp map[string]intmpls : map[string]int{"one&quo…

不用写代码也能开发,产品经理是怎么做到的?

产品经理再也不用求开发了……就在前几天&#xff0c;我做的小程序上线了&#xff01; 从产品原型设计&#xff0c;前端开发后端开发&#xff0c;产品部署到运维&#xff0c;都是由我1个人完成的。 我是啥时候学会写代码的呢&#xff1f;不瞒你说&#xff0c;我一行代码都没写…

基于卷积神经网络CNN的甘蔗芽体自动识别,卷积神经网络分类预测

目录 背影 卷积神经网络CNN的原理 卷积神经网络CNN的定义 卷积神经网络CNN的神经元 卷积神经网络CNN的激活函数 卷积神经网络CNN的传递函数 卷积神经网络CNN甘蔗芽体自动识别 基本结构 主要参数 MATALB代码 结果图 展望 背影 现在生活&#xff0c;为节能减排&#xff0c;减少…

LauterBach使用教程

工作需要&#xff0c;使用到劳得巴赫&#xff0c;但是公司只买了调试器&#xff0c;却没有买教程&#xff0c;所以就只能自己摸索和网上搜索这两种途径来学习。 注意&#xff1a;lauterbach可以使用命令来操作&#xff0c;但是由于本人刚刚使用&#xff0c;目前基本上使用的都…

[考前冲刺]计算机三级网络技术复习知识点总结·计算机三级网络技术重难点考前冲刺和解题技巧

选择题第一章重难点一、网络层次结构的功能①核心交换层的基本功能&#xff1a;1、核心交换层将多个汇聚层连接起来&#xff0c;为汇聚层的网络提供高速转发&#xff0c;为整个城域网提供一个高速、安全与具有QoS保障能力的数据传输环境&#xff1b;2、核心交换层实现与主干网络…

2023最新版会声会影更新下载及功能介绍

会声会影&#xff08;Corel VideoStudio&#xff09;为加拿大Corel发布的一款功能丰富的视频编辑软件。会声会影2023简单易用&#xff0c;具有史无前例的强大功能&#xff0c;拖放式标题、转场、覆叠和滤镜&#xff0c;色彩分级、动态分屏视频和新增强的遮罩创建器&#xff0c;…

Nuxt项目配置、目录结构说明-实战教程基础-Day02

Nuxt项目配置、目录结构说明-实战教程基础-Day02一、Nuxt项目结构1.1资源目录1.2 组件目录1.3 布局目录1.4 中间件目录1.5 页面目录1.6 插件目录1.7 静态文件目录1.8 Store 目录1.9 nuxt.config.js 文件1.10 package.json 文件其他&#xff1a;别名二、项目配置2.1 build2.2 cs…

0108检测环-无向图-数据结构和算法(Java)

文章目录1 API2 实现和分析3 测试后记1 API 检测一幅图是否还有环&#xff0c;如果有找出环路&#xff08;任意一条&#xff09;&#xff0c;API如下&#xff1a; public classCycleCycle(Grpah G)预处理函数booleanhasCycle()Iterable<Interge>cycle()有环给出环路&am…