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

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

实现效果

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...
相关推荐
ConardLi12 分钟前
Easy Dataset 已经突破 11.5K Star,这次又带来多项功能更新!
前端·javascript·后端
芒克芒克16 分钟前
ssm框架之Spring(上)
java·后端·spring
冴羽17 分钟前
10 个被严重低估的 JS 特性,直接少写 500 行代码
前端·javascript·性能优化
rising start19 分钟前
四、CSS选择器(续)和三大特性
前端·css
冒泡的肥皂24 分钟前
MVCC初学demo(二
数据库·后端·mysql
追逐时光者27 分钟前
一款基于 .NET WinForm 开源、轻量且功能强大的节点编辑器,采用纯 GDI+ 绘制无任何依赖库仅仅100+Kb
后端·.net
鬼火儿31 分钟前
1.2 redis7.0.4安装与配置开机自启动
java·后端
一 乐39 分钟前
高校后勤报修系统|物业管理|基于SprinBoot+vue的高校后勤报修系统(源码+数据库+文档)
java·前端·javascript·数据库·vue.js·毕设
爱喝水的小周40 分钟前
《UniApp 页面配置文件pages.json》
前端·uni-app·json
mapbar_front1 小时前
React中useContext的基本使用和原理解析
前端·react.js