Tinymce富文本编辑器二次开发电子病历时解决的bug

前言

本文是在Tinymce富文本编辑器添加自定义toolbar,二级菜单,自定义表单,签名的基础之上进行一些bug记录,功能添加,以及模版的应用和打印

项目描述

  1. 建立电子病历模版---录入(电子病历模版和电子病历打印模版)---查看电子病历和打印病历模版
  2. 建立电子病历----添加一个电子病历可以添加多个电子病历模版----并输入设置的值提交以key和value的组成的数组结构,提交,并在编辑时返显电子病历模版和数据----点击打印按钮,调出打印模版,赋值进行打印。

一、模版应用及打印

  1. 模版的应用
    点击新增电子病历,选择用户,选择电子模版,展示。此时电子病历只填值,此处需要优化。
    选择完顾客,顾客的信息需要返显在电子模版上,
  • 解决的问题:

    模版返显---拿到模版的key和value组成的数组---然后赋值---更新模版---在页面中展示

  • 引出的问题:

    1)输入后每次光标会到最左侧,输入出错。

    原因封装的编辑器每次更新之后都会通过模版的key和value组成的数组,然后重新设置模版,所以光标返回最左侧

    解决方法,定义变量只有一次拿到模版的key和value组成的数组,其他变更不触发emit返回新的数组,不重新给模版赋值

    编辑组件的应用

    cpp 复制代码
    <Editor
        ref="editorTinymce"
         height="900"
         v-model="templateData"  // 模版的html
         :templatevalue="templatevalue" // 模版的key和value组成的数组
         :isHide="true" // 是否展示菜单栏和toolbar栏
         v-if="!!templateData" // 模版展示条件
         :templateData="templateData" // 模版的html
         :getParams="getParams" // 是否需要获取key和value组成的数组
         @templateparams="handleTemplateParams" // 模版返回的key和value组成的数组
       />
  1. 模版的赋值
    点击左侧已添加的病历,返显模版以及将模版之前输入的值进行返显
  • 引出的问题: 1)切换模版,赋值有时不更新

    赋值分2种,1.初始化赋值,2.变更赋值。

    解决方法,第一次点击为初始化赋值,之后的点击为变更赋值,切换已添加的模版并没有销毁编辑器,所以是变更。

  1. 模版的打印
    点击编辑页打印---调出打印模版---将录入模版的值赋值给打印模版
    首先:输入模版和打印模版对应的值需要设置对应的key,如果不一样会赋值失败,此处属于初化始化赋值。

    点击列表操作列的打印---弹出所有关联的模版---选中模版,给打印模版赋值,进行打印预览
  2. 模版的销毁
    关闭弹窗之后,需要销毁模版,否者再次打开时之前打开的模版还在。

封装组件代码

cpp 复制代码
<template>
  <div :class="prefixCls" :style="{ width: containerWidth }">
    <ImgUpload
      :fullscreen="fullscreen"
      @uploading="handleImageUploading"
      @done="handleDone"
      v-if="showImageUpload && !props.isHide"
      v-show="editorRef"
      :disabled="disabled"
      :uploadParams="props.uploadParams"
    />
    <textarea :id="tinymceId" ref="elRef" :style="{ visibility: 'hidden' }" v-if="!initOptions.inline"></textarea>
    <slot v-else></slot>
    <signModal @register="signatureModal" @success="handleSignature" @exportSign="getSign" />
    <recordModal @register="recorderModal" @success="handleGetText" />
  </div>
</template>

<script lang="ts">
import sign from '/@/assets/svg/sign.svg';
import recordSvg from '/@/assets/svg/record.svg';
import type { Editor, RawEditorSettings, BodyComponentSpec } from 'tinymce';
import { useMessage } from '/@/hooks/web/useMessage';
import tinymce from 'tinymce/tinymce';
import 'tinymce/themes/silver';
import 'tinymce/icons/default/icons';
import 'tinymce/plugins/advlist';
import 'tinymce/plugins/anchor';
import 'tinymce/plugins/autolink';
import 'tinymce/plugins/autosave';
import 'tinymce/plugins/code';
import 'tinymce/plugins/codesample';
import 'tinymce/plugins/directionality';
import 'tinymce/plugins/fullscreen';
import 'tinymce/plugins/hr';
import 'tinymce/plugins/insertdatetime';
import 'tinymce/plugins/link';
import 'tinymce/plugins/lists';
import 'tinymce/plugins/media';
import 'tinymce/plugins/nonbreaking';
import 'tinymce/plugins/noneditable';
import 'tinymce/plugins/pagebreak';
import 'tinymce/plugins/paste';
import 'tinymce/plugins/preview';
import 'tinymce/plugins/print';
import 'tinymce/plugins/save';
import 'tinymce/plugins/searchreplace';
import 'tinymce/plugins/spellchecker';
import 'tinymce/plugins/tabfocus';
import 'tinymce/plugins/table';
import 'tinymce/plugins/template';
import 'tinymce/plugins/textpattern';
import 'tinymce/plugins/visualblocks';
import 'tinymce/plugins/visualchars';
import 'tinymce/plugins/wordcount';
// import '/@/components/MedicalTinymce/plugins/control/index.js';

import { defineComponent, computed, nextTick, ref, unref, watch, onDeactivated, onBeforeUnmount, toRaw } from 'vue';
import ImgUpload from './ImgUpload.vue';
import { toolbar, plugins } from './tinymce';
import { buildShortUUID } from '/@/utils/uuid';
import { bindHandlers } from './helper';
import { useModal } from '/@/components/Modal';
import { ActionEnum, VALIDATE_API } from '/@/enums/commonEnum';
import { onMountedOrActivated } from '/@/hooks/core/onMountedOrActivated';
import { useDesign } from '/@/hooks/web/useDesign';
import { isNumber } from '/@/utils/is';
import { useLocale } from '/@/locales/useLocale';
import { useAppStore } from '/@/store/modules/app';
import { asyncFindDefUrlById, asyncFindUrlById } from '/@/api/lamp/file/upload';
import signModal from '/@/components/Signature/components/signModal/index.vue';
import recordModal from '/@/components/CustomRecorder/index.vue';
const tinymceProps = {
  options: {
    type: Object as PropType<Partial<RawEditorSettings>>,
    default: () => ({}),
  },
  value: {
    type: String,
  },

  toolbar: {
    type: Array as PropType<string[]>,
    default: toolbar,
  },
  plugins: {
    type: Array as PropType<string[]>,
    default: plugins,
  },
  modelValue: {
    type: String,
  },
  height: {
    type: [Number, String] as PropType<string | number>,
    required: false,
    default: 400,
  },
  width: {
    type: [Number, String] as PropType<string | number>,
    required: false,
    default: 'auto',
  },
  showImageUpload: {
    type: Boolean,
    default: true,
  },
  isDef: {
    type: Boolean,
    default: false,
  },
  uploadParams: {
    type: Object as PropType<any>,
    default: {},
  },
  isHide: {
    type: Boolean,
    default: false,
  },
  isPrint: {
    type: Boolean,
    default: false,
  },
  templatevalue: {
    type: Array,
  },
  getParams: {
    type: Boolean,
    default: false,
  },
};
export default defineComponent({
  name: 'Tinymce',
  components: { ImgUpload, signModal, recordModal },
  inheritAttrs: false,
  props: tinymceProps,
  emits: ['change', 'update:modelValue', 'inited', 'init-error', 'templateparams'],
  setup(props, { emit, attrs }) {
    const { createMessage } = useMessage();
    const editorRef = ref<Nullable<Editor>>(null);
    const fullscreen = ref(false);
    const tinymceId = ref<string>(buildShortUUID('tiny-vue'));
    const elRef = ref<Nullable<HTMLElement>>(null);
    let dialogConfig = ref(null);
    const { prefixCls } = useDesign('tinymce-container');
    const [signatureModal, { openModal: openSignModal }] = useModal();
    const [recorderModal, { openModal: openRecord }] = useModal();
    const appStore = useAppStore();
    const appEnv = import.meta.env.MODE;
    let currentBookMark = ref<any>('');

    const tinymceContent = computed(() => props.modelValue);
    const childBtn = {
      type: 'grid', // component type
      columns: 1, // number of columns
      items: [
        {
          type: 'button',
          name: 'add',
          text: '添加子项',
        },
        {
          type: 'button',
          name: 'del',
          text: '删除子项',
        },
        {
          type: 'collection', // component type
          name: 'collection', // identifier
          label: '',
        },
        {
          type: 'collection', // component type
          name: 'collection1', // identifier
          label: '',
        },
      ], // array of panel components
    };
    let childItem = {
      type: 'grid', // component type
      columns: 1, // number of columns
      items: [
        {
          type: 'grid',
          columns: 2,
          items: [
            {
              type: 'input',
              name: 'label1',
              label: '标签1',
            },
            {
              type: 'input',
              name: 'value1',
              label: '值1',
            },
          ],
        },
      ], // array of panel components
    };
    const containerWidth = computed(() => {
      const width = props.width;
      if (isNumber(width)) {
        return `${width}px`;
      }
      return width;
    });

    const skinName = computed(() => {
      return appStore.getDarkMode === 'light' ? 'oxide' : 'oxide-dark';
    });

    const langName = computed(() => {
      const lang = useLocale().getLocale.value;
      return ['zh_CN', 'en'].includes(lang) ? lang : 'zh_CN';
    });

    const initOptions = computed((): RawEditorSettings => {
      const { height, options, toolbar, plugins } = props;
      const publicPath = import.meta.env.VITE_PUBLIC_PATH || '/';

      return {
        selector: `#${unref(tinymceId)}`,
        height,
        // toolbar: appEnv === 'development' ? [...toolbar, 'HtmlBtn'] : toolbar,
        toolbar: !!props.isHide ? false : !!props.isPrint ? false : toolbar,
        menubar: !!props.isHide ? false : !!props.isPrint ? 'print' : 'file edit insert view format table',
        menu: {
          print: {
            title: '打印',
            items: 'print',
          },
        },
        plugins,
        fontsize_formats: '8pt 10pt 12pt 14pt 16pt 18pt 20pt 22pt 24pt 36pt',
        font_formats: `微软雅黑='微软雅黑';宋体='宋体';黑体='黑体';仿宋='仿宋';楷体='楷体';隶书='隶书';幼圆='幼圆';
        Andale Mono=andale mono,times;Arial=arial,helvetica,sans-serif;Arial Black=arial black,avant garde;
        Book Antiqua=book antiqua,palatino;Comic Sans MS=comic sans ms,sans-serif;Courier New=courier new,
        courier;Georgia=georgia,palatino;Helvetica=helvetica;Impact=impact,chicago;Symbol=symbol;Tahoma=tahoma,
        arial,helvetica,sans-serif;Terminal=terminal,monaco;Times New Roman=times new roman,times;Trebuchet MS=trebuchet
        ms,geneva;Verdana=verdana,geneva;Webdings=webdings;Wingdings=wingdings`,
        language_url: publicPath + 'resource/tinymce/langs/' + langName.value + '.js',
        language: langName.value,
        branding: false,
        default_link_target: '_blank',
        link_title: false,
        object_resizing: false,
        auto_focus: true,
        skin: skinName.value,
        skin_url: publicPath + 'resource/tinymce/skins/ui/' + skinName.value,
        content_css: publicPath + 'resource/tinymce/skins/ui/' + skinName.value + '/content.min.css',
        ...options,
        extended_valid_elements: 'a[class|target|href|onclick],div[class|onclick|id|style],link[rel|href]',
        setup: (editor: Editor) => {
          console.log(editor, 'editoreditoreditoreditoreditor');
          editorRef.value = editor;
          editor.on('init', (e) => initSetup(e));
          // 注册一个icon
          editor.ui.registry.addIcon(
            'shopping-cart',
            `<svg viewBox="0 0 1024 1024" data-icon="shopping-cart" width="1.5em" height="1.5em" fill="currentColor" aria-hidden="true" focusable="false" class=""><path d="M922.9 701.9H327.4l29.9-60.9 496.8-.9c16.8 0 31.2-12 34.2-28.6l68.8-385.1c1.8-10.1-.9-20.5-7.5-28.4a34.99 34.99 0 0 0-26.6-12.5l-632-2.1-5.4-25.4c-3.4-16.2-18-28-34.6-28H96.5a35.3 35.3 0 1 0 0 70.6h125.9L246 312.8l58.1 281.3-74.8 122.1a34.96 34.96 0 0 0-3 36.8c6 11.9 18.1 19.4 31.5 19.4h62.8a102.43 102.43 0 0 0-20.6 61.7c0 56.6 46 102.6 102.6 102.6s102.6-46 102.6-102.6c0-22.3-7.4-44-20.6-61.7h161.1a102.43 102.43 0 0 0-20.6 61.7c0 56.6 46 102.6 102.6 102.6s102.6-46 102.6-102.6c0-22.3-7.4-44-20.6-61.7H923c19.4 0 35.3-15.8 35.3-35.3a35.42 35.42 0 0 0-35.4-35.2zM305.7 253l575.8 1.9-56.4 315.8-452.3.8L305.7 253zm96.9 612.7c-17.4 0-31.6-14.2-31.6-31.6 0-17.4 14.2-31.6 31.6-31.6s31.6 14.2 31.6 31.6a31.6 31.6 0 0 1-31.6 31.6zm325.1 0c-17.4 0-31.6-14.2-31.6-31.6 0-17.4 14.2-31.6 31.6-31.6s31.6 14.2 31.6 31.6a31.6 31.6 0 0 1-31.6 31.6z"></path></svg>`,
          );
          // 注册获取html以及数据的按钮
          registerSignBtn(editor);
        },
        // 生命周期:挂载后回调
        init_instance_callback: (editor: Editor) => {
          // 修改编辑器默认字体和字号
          editor.getBody().style.fontSize = '16pt';
          editor.getBody().style.fontFamily = '宋体';
        },
      };
    });

    // 注册获取html以及数据的按钮
    function registerSignBtn(editor: Editor) {
      editor.ui.registry.addButton('CardBtn', {
        type: 'button',
        // icon: `shopping-cart`,
        text: '获取并保存html',
        onAction: function (_) {
          //按钮事件:组装 html + data数据
          getControlValue();
          saveTemplate(editor.getContent(), getControlValue());
        },
      });
    }

    // 获取控件数据值
    function getControlValue() {
      let dom = tinymce.activeEditor.dom;
      let controls = dom.select('.control');
      let data = controls.map((item) => {
        // console.log('item', item);
        let dataControl = JSON.parse(item.getAttribute('data-control'));
        let controlValue = item.getAttribute('data-value');
        //文本框 没有data-value
        console.log(controlValue, item.firstElementChild.innerHTML, 'item.firstElementChild.innerHTML');
        if (!controlValue) {
          if (dataControl.initialData.select == 'input') {
            if (!!item.firstElementChild.innerHTML) {
              controlValue = item.firstElementChild.innerHTML;
            } else {
              controlValue = '';
            }
          }
          // controlValue = item.firstElementChild.innerHTML;
        }
        return {
          controlType: dataControl.initialData.select,
          fieldName: dataControl.initialData.name,
          fieldKey: dataControl.initialData.fieldKey,
          controlValue,
        };
      });
      console.log(data);
      return data;
    }
    // 保存模板
    async function saveTemplate(doc: string, data: any) {
      try {
        const params: any = { doc, data };

        emit('templateparams', params);
      } finally {
      }
    }

    const disabled = computed(() => {
      const { options } = props;
      const getdDisabled = options && Reflect.get(options, 'readonly');
      const editor = unref(editorRef);
      if (editor) {
        editor.setMode(getdDisabled ? 'readonly' : 'design');
      }
      return getdDisabled ?? false;
    });
    watch(
      () => attrs.disabled,
      () => {
        const editor = unref(editorRef);
        if (!editor) {
          return;
        }
        editor.setMode(attrs.disabled ? 'readonly' : 'design');
      },
    );

    onMountedOrActivated(() => {
      if (!initOptions.value.inline) {
        tinymceId.value = buildShortUUID('tiny-vue');
      }
      nextTick(() => {
        setTimeout(() => {
          initEditor();
        }, 30);
      });
    });

    onBeforeUnmount(() => {
      destory();
    });

    onDeactivated(() => {
      destory();
    });

    function destory() {
      if (tinymce !== null) {
        tinymce?.remove?.(unref(initOptions).selector!);
      }
    }
    // 弹框配置
    let Dialog = (editor) => {
      return {
        title: '添加控件', // The dialog's title - displayed in the dialog header
        body: {
          type: 'panel', // The root body type - a Panel or TabPanel
          items: [
            // A list of panel components
            {
              type: 'selectbox',
              name: 'select',
              label: '控件',
              items: [
                { value: 'input', text: '输入框' },
                { value: 'date', text: '日期' },
                { value: 'radio', text: '单选框' },
                { value: 'checkbox', text: '多选框' },
                { value: 'select', text: '下拉' },
                { value: 'sign', text: '签名' },
                { value: 'record', text: '语音转换' },
                // { value: 'textarea', text: 'textarea' },
              ],
            },
            {
              type: 'input', //类型可以是 checkbox, input, selectbox, textarea and urlinput
              name: 'name',
              label: '字段名称',
            },
            {
              type: 'input', //类型可以是 checkbox, input, selectbox, textarea and urlinput
              name: 'fieldKey',
              label: '字段key值',
            },
            {
              type: 'selectbox', //类型可以是 checkbox, input, selectbox, textarea and urlinput
              name: 'isShow',
              label: '字段名称是否展示',
              items: [
                { value: '1', text: '展示' },
                { value: '2', text: '不展示' },
              ],
            },
            // {
            //     type: 'checkbox',
            //     name:'checkbox',
            //     label:'checkbox'
            // },
            // {
            //     type: 'button',
            //     name:'tianjia',
            //     text:'添加子项',
            //     disabled:false
            // },
            // {
            //     type: 'htmlpanel', // A HTML panel component
            //     html: '11'
            // },
          ],
        },
        //初始值
        // initialData: {
        //   name: '2'
        // },
        buttons: [
          // A list of footer buttons
          {
            type: 'cancel',
            name: 'closeButton',
            text: '取消',
          },
          {
            type: 'submit',
            primary: true,
            text: '确认',
          },
        ],
        // radio select checkbox 添加子项
        onAction: dialogFn.onAction(editor),
        // 切换表单控件
        onChange: dialogFn.onChange(editor),
        onSubmit: dialogFn.onSubmit(editor),
      };
    };

    function initEditor() {
      const el = unref(elRef);
      if (el) {
        el.style.visibility = '';
      }
      tinymce
        .init(unref(initOptions))
        .then((editor) => {
          emit('inited', editor);
        })
        .catch((err) => {
          emit('init-error', err);
        });
      tinymce.PluginManager.add('control', function (editor) {
        dialogConfig.value = Dialog(editor);
        const dialogOpener = () => {
          return editor.windowManager.open(dialogConfig.value);
        };
        editor.ui.registry.addButton('control', {
          icon: 'non-breaking', //图标
          tooltip: '插入控件', //提示
          text: '控件库',
          onAction: function () {
            dialogOpener();
          },
        });
        setupButtons(editor);
        if (!props.isHide) {
          addToEditor(editor);
        }
        initBindEvent(editor);
      });
    }
    // 为初始化内容中控件绑定事件 和 控件值回填
    let initBindEvent = (editor) => {
      console.log(props.templatevalue, 'propspropspropsprops');
      let templateValue = toRaw(props.templatevalue);
      editor.on('init', () => {
        //绑定事件
        bindEvent(editor);
        //控件值回填
        let dom = tinymce.activeEditor.dom;
        let controls = dom.select('.control');
        controls.forEach((item) => {
          let dataControl = JSON.parse(item.getAttribute('data-control'));
          let controlValue = item.getAttribute('data-value');
          if (!!templateValue && templateValue.length > 0) {
            templateValue.forEach((i) => {
              if (i.fieldKey == dataControl.initialData.fieldKey) {
                controlValue = i.controlValue;
                switch (dataControl.initialData.select) {
                  case 'input':
                    if (!!controlValue) {
                      console.log(item, item.firstElementChild, 'itemitemitemitemitem');
                      if (!!item.firstElementChild) {
                        item.firstElementChild.innerText = controlValue;
                      }
                      // item.setAttribute('data-value', controlValue);
                    }
                    break;
                  case 'radio':
                    if (!!controlValue) {
                      item.querySelector('[value="' + controlValue + '"]').checked = true;
                    }
                    break;
                  case 'checkbox':
                    let checkboxs = item.querySelectorAll('input');
                    checkboxs.forEach((it) => {
                      if (controlValue.split(',').includes(it.value)) {
                        it.checked = true;
                      }
                    });
                    break;
                  case 'select':
                    item.querySelector('[value=' + controlValue + ']').selected = true;
                    break;
                }
              }
            });
          } else {
            switch (dataControl.initialData.select) {
              case 'radio':
                if (!!controlValue) {
                  item.querySelector('[value="' + controlValue + '"]').checked = true;
                }
                break;
              case 'checkbox':
                let checkboxs = item.querySelectorAll('input');
                checkboxs.forEach((it) => {
                  if (controlValue.split(',').includes(it.value)) {
                    it.checked = true;
                  }
                });
                break;
              case 'select':
                item.querySelector('[value=' + controlValue + ']').selected = true;
                break;
            }
          }
          console.log(controlValue);
        });
      });
    };

    let addToEditor = (editor) => {
      // 添加悬浮 上下文工具栏
      editor.ui.registry.addContextToolbar('editcontrol', {
        //触发条件
        predicate: function (node) {
          console.log(node, props.isHide, 'isHideisHideisHide');
          // alert(node);
          // if (node.className == 'c-sign') {
          //   openSignModal(true, {
          //     type: ActionEnum.ADD,
          //   });
          // }
          return !props.isHide && node.className === 'control';
          // return !props.isHide && node.className === 'control' && node.nodeName.toLowerCase() === 'span';
        },
        items: 'changecontrol removecontrol', //显示的工具列表
        position: 'selection', //工具栏放置位置  selection node line
        // scope: 'node',
      });
    };
    // 弹框中的方法
    let dialogFn = {
      // radio select checkbox 添加子项编辑控件
      onAction: (edntor) => (dialogApi, details) => {
        let data = dialogApi.getData();

        if (details.name == 'add') {
          addChildItem(dialogApi);
        }
        if (details.name == 'del') {
          let items = dialogConfig.value.body.items;
          items[items.length - 1].items.pop();
        }
        dialogConfig.value.initialData = data;
        dialogApi.redial(dialogConfig.value);
      },
      // 控件弹窗选择控件Change事件
      onChange: (editor) => (dialogApi, details) => {
        console.log(dialogConfig.value.body, details, dialogApi);
        if (dialogConfig.value.title == '编辑控件') return;

        let data = dialogApi.getData();
        // dialogConfig.body.items[4].html = formControl[data.select]

        if (data.select == 'input' || data.select == 'date' || data.select == 'textarea') {
          dialogConfig.value.body.items.splice(4);
          // dialogApi.redial(dialogConfig.value);
          // dialogApi.setData(data);
        }
        if (
          data.select != 'input' &&
          data.select != 'date' &&
          data.select != 'textarea' &&
          data.select != 'sign' &&
          data.select != 'record' &&
          !dialogConfig.value.body.items[4]
        ) {
          let btns = JSON.parse(JSON.stringify(childBtn));

          let items = JSON.parse(JSON.stringify(childItem));
          dialogConfig.value.body.items.splice(4, 0, btns, items);
          dialogApi.redial(dialogConfig.value);
          dialogApi.setData(data);
        }
        // dialogApi.redial(dialogConfig.value);
        // if (details.name == 'select') dialogApi.redial(dialogConfig.value); //重新渲染dialog
        // dialogApi.setData(data);
        // dialogApi.focus(); // 聚焦
        console.log('dataTdataTdataT', data);
      },
      // 控件弹窗确认回调事件
      onSubmit: (editor) => (api) => {
        let control = '';
        let data = api.getData();
        let controlName = data.select + getId();
        // 输入框
        if (data.select == 'input') {
          control = `<span contenteditable="true" style="display:inline-block;
        min-width:100px;border-bottom:1px solid black;outline: none;padding: 0" name=${controlName} type="${data.select}"> </span>`;
        } else if (data.select == 'date') {
          // 日期
          control = `<input class="c-form" contenteditable="true" style='border: none;border-bottom: 1px solid' name=${controlName} type="datetime-local" value="${data.name}" />`;
        } else if (data.select == 'textarea') {
          //文本御
          control = `<textarea class="c-form" contenteditable="true" rows="2" cols="30" style="display:inline-block;
        min-width:100px;border-bottom:1px solid;"  name=${controlName}></textarea>`;
        }
        // if(data.select == 'input' || data.select == 'date'){
        //   control = `<input contenteditable="true" name=${controlName} type="${data.select}" value="${data.name}" />`
        // }
        else if (data.select == 'radio' || data.select == 'checkbox') {
          let l = dialogConfig.value.body.items[dialogConfig.value.body.items.length - 1].items.length;
          for (let i = 1; i <= l; i++) {
            control =
              control +
              `<label contenteditable="true"><input class="c-form" name=${controlName} type=${data.select} value=${data['value' + i]} /> ${
                data['label' + i]
              }</label>`;
          }
        } else if (data.select == 'select') {
          // 下拉
          let l = dialogConfig.value.body.items[dialogConfig.value.body.items.length - 1].items.length;
          for (let i = 1; i <= l; i++) {
            control = control + `<option value=${data['value' + i]} label=${data['label' + i]}></option>`;
          }
          control = `<select class="c-form" contenteditable="true" style='border: none;border-bottom: 1px solid;padding: 5px 0px 5px 5px' name=${controlName}>${control}</select>`;
        } else if (data.select === 'sign') {
          control = `<img style="width: 32px;height: 32px;" class='c-sign' src='${sign}'></img>`;
        } else if (data.select === 'record') {
          control = `<img style="width: 32px;height: 32px;" class='c-record' src='${recordSvg}'></img>`;
        }
        // 通用 dom 结构和样式
        control = `${
          !!data.name && data.isShow == '1' && dialogConfig.value.title != '编辑控件' ? `<span>${data.name}</span>:` : ``
        }<span class="control" id="span1"
        style="display:inline-block;margin:0 5px;background-color: #f1f1f1;"
        contenteditable="false" data-control=${JSON.stringify({ body: dialogConfig.value.body, initialData: data })} data-value="">
        ${control}
        <span class="c-menu" style="padding:0 5px;"></span>
        </span></span>`;
        // console.log(editor.selection.getNode())
        if (editor.selection.getContent()) {
          //编辑控件
          // editor.selection.getNode().parentNode.removeChild(editor.selection.getNode());
          editor.selection.setContent(control);
          // editor.insertContent(control)
        } else {
          //添加控件
          editor.insertContent(control);
        }

        bindEvent(editor);
        api.close();
      },
    };
    // 为控件绑定事件
    let bindEvent = (editor) => {
      console.log(navigator.userAgent, 'editoreditoreditor');

      let dom = tinymce.activeEditor.dom;
      // console.log(dom.select('#span1'))
      setTimeout(() => {
        // dom.bind(dom.select('.c-menu'), 'click', (e) => {
        //   // 显示指定的上下文菜单
        //   editor.dispatch('contexttoolbar-show', { toolbarKey: 'editcontrol' });
        //   // 隐藏指定的上下文菜单
        //   editor.dispatch('contexttoolbar-hide', { toolbarKey: 'editcontrol' });

        //   e.stopPropagation();
        // });
        dom.bind(dom.select('.c-form'), 'change', (e) => {
          if (e.target.type == 'date') {
            e.target.parentNode.setAttribute('data-value', e.target.value);
            e.target.setAttribute('value', e.target.value);
          }
          if (e.target.type == 'radio') {
            e.target.parentNode.parentNode.querySelectorAll('label').forEach((item) => {
              item.querySelector('input').removeAttribute('checked');
            });
            e.target.parentNode.parentNode.setAttribute('data-value', e.target.value);
            e.target.setAttribute('checked', 'checked');
          }
          if (e.target.type == 'checkbox') {
            let checkedArr = [];
            let parentSpan = e.target.parentNode.parentNode;
            // ---------------------- 值响应有问题
            parentSpan.querySelectorAll('label').forEach((item) => {
              let checkbox = item.querySelector('input');
              // checkbox.removeAttribute('checked')
              if (checkbox.checked) {
                checkedArr.push(checkbox);
                // checkbox.setAttribute('checked','checked')
              }
            });
            parentSpan.setAttribute('data-value', checkedArr.toString());
          }
          if (e.target.localName == 'select') {
            e.target.querySelectorAll('option').forEach((item) => {
              item.removeAttribute('selected');
              if (item.selected) {
                item.setAttribute('selected', 'selected');
              }
            });
            e.target.parentNode.setAttribute('data-value', e.target.value);
          }
        });

        // 单独为 日期 控件绑定失焦事件,change不好使
        dom.bind(dom.select('.c-form'), 'blur', (e) => {
          if (e.target.type == 'datetime-local') {
            const time = new Date(e.target.value).toLocaleString();
            e.target.parentNode.setAttribute('data-value', time);
            e.target.setAttribute('value', e.target.value);
          }
        });

        //签名点击事件
        dom.bind(dom.select('.c-sign'), `click`, () => {
          currentBookMark.value = editor.selection.getBookmark(); //记录点击的位置,弹框结束后插入签名
          openSignModal(true, {
            type: ActionEnum.ADD,
          });
          // editor.selection.setContent(sign);  //把签名插入编辑器
        });
        // 签名ipad touch事件
        dom.bind(dom.select('.c-sign'), `touchstart`, (e) => {
          console.log(e);
          currentBookMark.value = editor.selection.getBookmark(); //记录点击的位置,弹框结束后插入签名
          openSignModal(true, {
            type: ActionEnum.ADD,
          });
          // editor.selection.setContent(sign);  //把签名插入编辑器
        });

        // 语音转换事件
        dom.bind(dom.select('.c-record'), `click`, () => {
          currentBookMark.value = editor.selection.getBookmark(); //记录点击的位置,弹框结束后插入文字
          openRecord(true, {
            type: ActionEnum.ADD,
          });
          // editor.selection.setContent(sign);  //把签名插入编辑器
        });
        // ipad touch事件
        dom.bind(dom.select('.c-record'), `touchstart`, (e) => {
          console.log(e);
          currentBookMark.value = editor.selection.getBookmark();
          openRecord(true, {
            type: ActionEnum.ADD,
          });
          // editor.selection.setContent(sign);  //把签名插入编辑器
        });
      }, 100);
    };
    // 添加子项 key value 方法
    function addChildItem(dialogApi) {
      console.log(dialogApi, 'dialogApidialogApidialogApidialogApi');
      let childItems = dialogConfig.value.body.items[dialogConfig.value.body.items.length - 1].items;
      childItems.push({
        type: 'grid', // component type
        columns: 2, // number of columns
        items: [
          {
            type: 'input',
            name: 'label' + (childItems.length + 1),
            label: '标签' + (childItems.length + 1),
          },
          {
            type: 'input',
            name: 'value' + (childItems.length + 1),
            label: '值' + (childItems.length + 1),
          },
        ], // array of panel components
      });
    }
    // 添加上下文工具栏
    let setupButtons = (editor) => {
      editor.ui.registry.addButton('changecontrol', {
        icon: 'edit-block',
        tooltip: '编辑控件',
        onAction: () => {
          console.log(editor);
          // editor.windowManager.open(dialogConfig)
          // editor.setContent('1213')
          // editor.selection.select(123)
          // editor.selection.setContent('<span>123</span>')
          // editor.selection.getNode()
          console.log(editor.selection.getNode());
          let data = editor.selection.getNode().getAttribute('data-control');
          // let name = editor.selection.getNode().querySelector('input').getAttribute('name')
          // let val = editor.selection.getNode().querySelectorAll('[name='+name+']').value
          let dataJson = JSON.parse(data);
          dialogConfig.value = Dialog(editor);
          // dialogConfig.value.body = data.body;
          dialogConfig.value.title = '编辑控件';
          dialogConfig.value.initialData = dataJson.initialData;
          if (dataJson.initialData.select == 'radio' || dataJson.initialData.select == 'checkbox') {
            let btns = JSON.parse(JSON.stringify(childBtn));
            var keyArr: string[] = [];
            for (var key in dataJson.initialData) {
              if (key.indexOf('label') > -1) {
                keyArr.push(key);
              }
            }
            console.log(keyArr);
            let obj = {
              type: 'grid', // component type
              columns: 1, // number of columns
              items: [],
            };
            keyArr.forEach((i) => {
              let index = i.indexOf('label');
              let strI = i.substring(5);
              console.log(i, index, strI, 'strIstrI');
              let itemObj = {
                type: 'grid',
                columns: 2,
                items: [
                  {
                    type: 'input',
                    name: i,
                    label: '标签' + strI,
                  },
                  {
                    type: 'input',
                    name: 'value' + strI,
                    label: '值' + strI,
                  },
                ],
              };
              obj.items.push(itemObj);
            });
            let items = JSON.parse(JSON.stringify(obj));
            dialogConfig.value.body.items.splice(4, 0, btns, items);
          }
          console.log(dialogConfig.value, 'dialogConfig.valuedialogConfig.valuedialogConfig.value');
          editor.windowManager.open(dialogConfig.value);
        },
      });
      editor.ui.registry.addButton('removecontrol', {
        icon: 'remove',
        tooltip: '删除控件',
        onAction: (editor) => {
          editor.selection.setContent('');
          editor.dispatch('contexttoolbar-hide', { toolbarKey: 'editcontrol' });
          console.log(arguments);
        },
      });
    };
    // 获取id name
    function getId() {
      return '_' + new Date().getTime();
    }
    function initSetup(e) {
      const editor = unref(editorRef);
      console.log(editor, 'editoreditoreditoreditor');
      if (!editor) {
        return;
      }
      const value = props.modelValue || '';
      editor.setContent(value);

      bindModelHandlers(editor);
      bindHandlers(e, attrs, unref(editorRef));
    }

    function setValue(editor: Recordable, val: string, prevVal?: string) {
      // console.log(editor, val, props.templatevalue, 'valvalvalval1111111111111111111111111');

      // console.log(controls, 'controlscontrolscontrols');
      if (editor && typeof val === 'string' && val !== prevVal && val !== editor.getContent({ format: attrs.outputFormat })) {
        editor.setContent(val);
      }
      if (!!props.getParams) {
        saveTemplate(editor.getContent(), getControlValue());
      }
    }

    function bindModelHandlers(editor: any) {
      const modelEvents = attrs.modelEvents ? attrs.modelEvents : null;
      const normalizedEvents = Array.isArray(modelEvents) ? modelEvents.join(' ') : modelEvents;

      watch(
        () => props.modelValue,
        (val: string, prevVal: string) => {
          console.log(val, '00000000000000000000');
          setValue(editor, val, prevVal);
        },
      );

      watch(
        () => props.value,
        (val, prevVal) => {
          console.log(val, prevVal, 'vvvvvvvvvvvvvvvvvvvv');
          setValue(editor, val, prevVal);
        },
        {
          immediate: true,
        },
      );
      watch(
        () => props.templatevalue,
        (val, prevVal) => {
          console.log(val, prevVal, '1111111111111111');
          // setTemplateData(editor, val);
        },
        {
          immediate: true,
        },
      );

      editor.on(normalizedEvents ? normalizedEvents : 'change keyup undo redo', () => {
        const content = editor.getContent({ format: attrs.outputFormat });
        emit('update:modelValue', content);
        emit('change', content);
      });

      editor.on('FullscreenStateChanged', (e) => {
        fullscreen.value = e.state;
      });
    }
    function setTemplateData(arr) {
      let templateValue = toRaw(arr);
      let dom = tinymce.activeEditor.dom;
      let controls = dom.select('.control');
      controls.forEach((item) => {
        let dataControl = JSON.parse(item.getAttribute('data-control'));
        let controlValue = item.getAttribute('data-value');
        if (!!templateValue && templateValue.length > 0) {
          templateValue.forEach((i) => {
            if (i.fieldKey == dataControl.initialData.fieldKey) {
              controlValue = i.controlValue;
              switch (dataControl.initialData.select) {
                case 'input':
                  if (!!controlValue) {
                    console.log(item, item.firstElementChild, 'itemitemitemitemitem');
                    if (!!item.firstElementChild) {
                      item.firstElementChild.innerText = controlValue;
                    }
                    // item.setAttribute('data-value', controlValue);
                  }
                  break;
                case 'radio':
                  if (!!controlValue) {
                    item.querySelector('[value="' + controlValue + '"]').checked = true;
                  }
                  break;
                case 'checkbox':
                  let checkboxs = item.querySelectorAll('input');
                  checkboxs.forEach((it) => {
                    if (controlValue.split(',').includes(it.value)) {
                      it.checked = true;
                    }
                  });
                  break;
                case 'select':
                  item.querySelector('[value=' + controlValue + ']').selected = true;
                  break;
              }
            }
          });
        } else {
          switch (dataControl.initialData.select) {
            case 'radio':
              if (!!controlValue) {
                item.querySelector('[value="' + controlValue + '"]').checked = true;
              }
              break;
            case 'checkbox':
              let checkboxs = item.querySelectorAll('input');
              checkboxs.forEach((it) => {
                if (controlValue.split(',').includes(it.value)) {
                  it.checked = true;
                }
              });
              break;
            case 'select':
              item.querySelector('[value=' + controlValue + ']').selected = true;
              break;
          }
        }
        console.log(controlValue);
      });
      // console.log(data, '000000000000001111111111111111111');
      // editor.setContent(content);
      // console.log(dom, '99999999999999999');
      console.log(controls[0], '666666666666666666666666');
    }
    function handleImageUploading(name: string) {
      const editor = unref(editorRef);
      if (!editor) {
        return;
      }
      editor.execCommand('mceInsertContent', false, getUploadingImgName(name));
      const content = editor?.getContent() ?? '';
      setValue(editor, content);
    }

    function handleDone(name: string, fileId: string) {
      const editor = unref(editorRef);
      if (!editor) {
        return;
      }
      const content = editor?.getContent() ?? '';

      if (fileId) {
        const api = props.isDef ? asyncFindDefUrlById : asyncFindUrlById;
        api(fileId).then((res) => {
          // bug: 这里返回的图片链接必须是永久有效的,否则会出现图片过期无法访问的情况。 暂时没好的解决方案
          if (res.code === 0) {
            const val = content?.replace(getUploadingImgName(name), `<img data-id="${fileId}" src="${res?.data}"/>`) ?? '';
            setValue(editor, val);
          }
        });
      } else {
        const val = content?.replace(getUploadingImgName(name), `<img data-path="${fileId}" src="${fileId}" alt="上传失败"/>`) ?? '';
        setValue(editor, val);
      }
    }

    function getUploadingImgName(name: string) {
      return `[uploading:${name}]`;
    }

    function clickSetValue(val) {
      const editor = unref(editorRef);
      setValue(editor, val);
    }
    function handleSignature(data, base64Data) {
      editorRef.value?.selection.moveToBookmark(currentBookMark.value); // 把光标位置摆正
      editorRef.value?.focus(); // 聚焦
      editorRef.value.execCommand(
        // 填充!搞定!
        'mceInsertContent',
        false,
        `<img style='height: 80px;width: 250px;' data-id="${data.id}" src="${data}" alt=""/>`,
      );
      // editorRef.value?.selection.setContent(`<img style='height: 80px;width: 250px' data-id="${data.id}" src="${data}" alt=""/>`);
    }
    // 获取语音转换后的文字
    function handleGetText(data?) {
      console.log(data);
    }
    function getSign(data: any) {
      console.log(data);
    }
    function getSubmitData() {
      const editor = unref(editorRef);
      let controlValue = getControlValue();

      let content = editor?.getContent() ?? '';
      let data = {
        doc: content,
        data: controlValue,
      };
      return data;
    }
    function destroyTiny() {
      tinymce.editors[unref(tinymceId)].destroy();
    }
    return {
      prefixCls,
      containerWidth,
      initOptions,
      signatureModal,
      tinymceContent,
      elRef,
      tinymceId,
      handleImageUploading,
      handleDone,
      editorRef,
      fullscreen,
      disabled,
      props,
      setValue,
      clickSetValue,
      handleSignature,
      getSign,
      recorderModal,
      handleGetText,
      getSubmitData,
      setTemplateData,
      destroyTiny,
    };
  },
});
</script>

<style lang="less" scoped></style>

<style lang="less">
@prefix-cls: ~'@{namespace}-tinymce-container';

.@{prefix-cls} {
  position: relative;
  line-height: normal;

  textarea {
    z-index: -1;
    visibility: hidden;
  }
}
</style>

总结

  1. 需要优化模版应用的时候,达到的效果,只输入,不能对其他的模版设置进行修改,目前可以对模版进行增删改。
  2. 模版打印的存在同1的问题
  3. tinymce的中文文档
  4. tinymce的英文Api
相关推荐
susu10830189111 分钟前
vue3中父div设置display flex,2个子div重叠
前端·javascript·vue.js
IT女孩儿1 小时前
CSS查缺补漏(补充上一条)
前端·css
BestandW1shEs1 小时前
谈谈Mysql的常见基础问题
数据库·mysql
重生之Java开发工程师1 小时前
MySQL中的CAST类型转换函数
数据库·sql·mysql
教练、我想打篮球1 小时前
66 mysql 的 表自增长锁
数据库·mysql
Ljw...1 小时前
表的操作(MySQL)
数据库·mysql·表的操作
哥谭居民00011 小时前
MySQL的权限管理机制--授权表
数据库
wqq_9922502771 小时前
ssm旅游推荐系统的设计与开发
数据库·旅游
难以触及的高度2 小时前
mysql中between and怎么用
数据库·mysql