公式规则编辑器,满足业务规则公式编辑场景

项目开发又要到特殊的场景,不是函数公式的编辑器,是业务需求的公式规则编辑器,如满足营收计算、项目系统计算 、一些特定场景计算,主要是字段来源于系统、已定义字段,满足基础可配置的场景需求

实现效果

1. 需求确认

基于原型需求,与产品、后端讨论出来了以下需求:

  • 基础编辑框与富文本编辑器接近, 编辑历史记录
  • 实现外部规则子项选择填入,规则子项在编辑区可删除(Delete)
  • 支持加减乘除,与或非,括号的选择填入
  • 支持任意基础中英文、符号的自由编辑
  • 编辑后显示以文本形式展示
  • 实际规则是以字段形式的规则公式提供后端使用
  • 公式校验由接口提供
  • 支持函数选择填入

方案确定

首先明确是以富文本形式实现,富文本可以基于contenteditable实现,原计划是参考钉钉的薪酬计算规则的编辑组件自己用contenteditable实现富文本组件,但是考虑到快照(snapshot),插入处理,回显等一些文字需要耗费不少时间,决定基于现有的第三方富文本库来实现该公式规则编辑器。

项目上的富文本是Tinymce, 查找文档后提供插入方法,直接决定在Tinymce基础上实现。

javascript 复制代码
    // 插入方法
    const editor = window.tinyMCE.editors[curId.value];
    editor.insertContent(value);

代码实现

富文本编辑器组件 tinymce-formula

向富文本插入内容

javascript 复制代码
    const insertContent = ({ value, type, offset, offsetIdx }) => {
      const editor = window.tinyMCE.editors[curId.value];
      if (type === 'operator') { //计算规则
        if (offset) {
          editor.insertContent(value);
          selectionSetRng(offsetIdx);
        } else {
          editor.insertContent(value);
        }
      } else if (type === 'field') { // 规则子项
        editor.insertContent(`<span class="mention-${type}" contenteditable="false">${`#${value}#`}</span>`);
      } else {
        editor.insertContent(value);
        offset && selectionSetRng(offsetIdx);
      }
      initInstanceCallback();
      editor.focus(); // 显示光标
    };

插入文本时改变光标位置

javascript 复制代码
   const selectionSetRng = (offsetIdx) => {
      const editor = window.tinyMCE.editors[curId.value];
      // 获取当前选区的范围对象
      const range = editor.selection.getRng();
      // 获取光标位置的索引
      const caretIndex = range.startOffset;
      // 计算光标位置的偏移量
      const offsetIndex = caretIndex - 1;

      // 设置光标位置
      editor.selection.setRng({
        startContainer: range.startContainer,
        startOffset: offsetIdx ?? offsetIndex,
        endContainer: range.startContainer,
        endOffset: offsetIdx ?? offsetIndex,
      });
    };

处理富文本内容,回调公式节点数组、文本

javascript 复制代码
    const contentChange = (val) => {
      const editor = window.tinyMCE?.editors[curId.value]; // 富文本实例
      if (!editor) return;
      const tempContainer = editor.getBody(); // 富文本节点内容
      const text = tempContainer.textContent;
      if (props.modelValue === text) return; // 内容全等不回调
      const childNodes = tempContainer.childNodes;
      const calcList = [];
      const regexText = props.operatorList.map((operator) => `\\${operator.value ?? operator}`).join('');
      const operatorRegex = new RegExp(`([${regexText}])`);
      const operatorSplitRegex = new RegExp(`([${regexText}])`);

      childNodes.forEach((p) => {
        p.childNodes.forEach((element) => {
          const classText = element.nodeType === 1 && element?.getAttribute ? element?.getAttribute('class') : '';
          const value = element.textContent;
          if (element.nodeType === 3) {
            if (value !== '') {
              calcList.push({
                type: 'text',
                value,
              });
            }
          } else if (classText && classText.indexOf('mention-field') !== -1) {
            if (value !== '') {
              calcList.push({
                type: 'field',
                value,
              });
            }
          } else if (classText && classText.indexOf('mention-operator') !== -1) {
            const operatorSplits = value.split(operatorRegex);
            operatorSplits.forEach((v) => {
              if (v !== '') {
                if (operatorSplitRegex.test(v)) {
                  calcList.push({
                    type: 'operator',
                    value: v,
                  });
                } else {
                  calcList.push({
                    type: 'text',
                    value: v,
                  });
                }
              }
            });
          } else {
            calcList.push({
              type: 'text',
              value,
            });
          }
        });
      });
      emit('changeHtmlContent', val);
      emit('change', calcList);
      emit('input:modelValue', text);
    };
公式规则编辑弹窗组件 formula-dialog
javascript 复制代码
/**
 * @description: 字段-点击插入
 */
const fieldClick = (row) => {
  if (row.code === curRow.value.code) return;
  contendEditRef.value.insertContent({ value: row.name, type: 'field' });
};

/**
 * @description: 操作符-点击插入
 */
const operatorClick = (m) => {
  if (typeof m === 'string') {
    contendEditRef.value.insertContent({ value: m, type: 'text' });
  } else {
    contendEditRef.value.insertContent({ ...m, type: 'text' });
  }
};

/**
 * @description: 函数-点击插入
 */
const functionClick = (m) => {
  contendEditRef.value.insertContent({ ...m, type: 'function' });
};

todo

  • 规则子项在编辑器可输入提出下拉框
  • 函数扩展
  • 规则嵌套

仓库链接

  1. github.com/ZTrainWilli...
相关推荐
开心工作室_kaic15 分钟前
ssm068海鲜自助餐厅系统+vue(论文+源码)_kaic
前端·javascript·vue.js
有梦想的刺儿35 分钟前
webWorker基本用法
前端·javascript·vue.js
cy玩具1 小时前
点击评论详情,跳到评论页面,携带对象参数写法:
前端
customer081 小时前
【开源免费】基于SpringBoot+Vue.JS周边产品销售网站(JAVA毕业设计)
java·vue.js·spring boot·后端·spring cloud·java-ee·开源
qq_390161772 小时前
防抖函数--应用场景及示例
前端·javascript
John.liu_Test2 小时前
js下载excel示例demo
前端·javascript·excel
Yaml42 小时前
智能化健身房管理:Spring Boot与Vue的创新解决方案
前端·spring boot·后端·mysql·vue·健身房管理
PleaSure乐事2 小时前
【React.js】AntDesignPro左侧菜单栏栏目名称不显示的解决方案
前端·javascript·react.js·前端框架·webstorm·antdesignpro