项目开发又要到特殊的场景,不是函数公式的编辑器,是业务需求的公式规则编辑器,如满足营收计算、项目系统计算 、一些特定场景计算,主要是字段来源于系统、已定义字段,满足基础可配置的场景需求
实现效果
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
- 规则子项在编辑器可输入提出下拉框
- 函数扩展
- 规则嵌套