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

发布于:2024-04-26 ⋅ 阅读:(15) ⋅ 点赞:(0)

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

实现效果

formula-rule-handle.gif

demo:

1. 需求确认

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

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

方案确定

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

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

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

代码实现

富文本编辑器组件 tinymce-formula

向富文本插入内容

    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(); // 显示光标
    };

插入文本时改变光标位置

   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,
      });
    };

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

    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
/**
 * @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

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

仓库链接