如何实现钉钉宜搭的函数编辑计算功能
前言:钉钉宜搭是阿里的低代码平台,函数编辑功能可以获取页面的组件,书写函数,使组件与参数参与计算 本文章参考# CodeMirror 6+ vue3 实现简单的计算公式,插入标签功能
本文章阅读需要有部分低代码使用基础
拆分功能
- 函数编辑框:这里使用的codemirror
import {EditorView, basicSetup} from "codemirror";
import {EditorState} from "@codemirror/state";
import {javascript} from "@codemirror/lang-javascript";
import {MatchDecorator} from "@codemirror/view";
import {PlaceholderWidget} from "./PlaceholderWidget.js";
./PlaceholderWidget.js
import {
WidgetType,
} from "@codemirror/view";
export class PlaceholderWidget extends WidgetType {
constructor(name) {
super();
this.name = name;
}
eq(other) {
return this.name == other.name;
}
toDOM() {
let elt = document.createElement("span");
elt.style.cssText = `
border-radius: 4px;
padding:5px;
color:#fff;
background: #1e90ff;`;
elt.textContent = this.name;
return elt;
}
ignoreEvent() {
return false;
}
}
- 页面元素列表:这里就自己想办法拿到页面上的组件
- 函数列表:这里需要自己书写函数列表以及具体的功能函数
- 函数介绍;函数介绍
第一步-代码编辑框
//页面元素
<div class="code-mirror" ref="codeMirror"></div>
//ref获取元素
const codeMirror = ref();
onMounted(() => {
comps.value = props.d.getComps()
const placeholderMatcher = new MatchDecorator({
// regexp: /[[(\w+)]]/g, // 原有逻辑
regexp: /[[(.+?)]]/g, //支持中文
decoration: (match) =>
Decoration.replace({
widget: new PlaceholderWidget(match[1]),
}),
});
const placeholders = ViewPlugin.fromClass(
class {
placeholders: DecorationSet;
constructor(view: EditorView) {
this.placeholders = placeholderMatcher.createDeco(view);
}
update(update: ViewUpdate) {
this.placeholders = placeholderMatcher.updateDeco(
update,
this.placeholders
);
}
},
{
decorations: (instance) => instance.placeholders,
provide: (plugin) =>
EditorView.atomicRanges.of((view) => {
return view.plugin(plugin)?.placeholders || Decoration.none;
}),
}
);
if (codeMirror.value) {
const baseTheme = EditorView.baseTheme({
".cm-mywidget": {
paddingLeft: "6px",
paddingRight: "6px",
paddingTop: "3px",
paddingBottom: "3px",
marginLeft: "3px",
marginRight: "3px",
backgroundColor: "#ffcdcc",
borderRadius: "4px",
},
});
let v = ''
if (props.d.func && typeof props.d.func != 'string') {
v = props.d?.func?.join('')
} else if (props.d.func && typeof props.d.func == 'string') {
v = props.d.func
}
editor = new EditorView({
state: EditorState.create({
doc: v,
extensions: [placeholders, baseTheme, basicSetup, javascript()],
}),
parent: codeMirror.value,
});
});
第二步-页面元素列表
这里就不用提供代码了
第三步-函数列表
//定义一个funs.js文件,里面写具体的函数
const func={
// 平均值
AVERAGE(...args) {
let sum = 0
args?.forEach(item => {
sum += item
})
return sum / args.length
},
// 合并文本
CONCATENATE(...args) {
return args.join('')
},
...
}
第五步-函数介绍
//定义列表以及介绍
const functions = ref([
{
name: '常用函数',
type: 'dic',
children: [
{
name: 'AVERAGE',
tag: '数字',
info: '函数可以获取一组数值的算数平均值',
usage: '(数字1,数字2,……)',
example: {
start: '(',
content: ['物理成绩', '化学成绩', '生物成绩'],
end: ')返回三门课程的平均分'
}
},
{
name: 'CONCATENATE',
tag: '文本',
info: '函数可以将多个文本合并成一个文本',
usage: '(文本1,文本2,……)',
example: {
start: '(“可视化搭建”,"平台")会返回"可视化搭建平台"',
}
},
{
name: 'IF',
tag: '范型',
info: '函数判断一个条件能否满足;如果满足返回一个值,如果不满足返回另外一个值',
usage: '(逻辑表达式,为true时返回的值,为false时返回的值)',
example: {
start: '(',
content: ['物理成绩'],
end: '>60,"及格","不及格"),当物理成绩>60时返回及格,否则返回不及格。'
}
},
...]
}
//这里粘贴一下我写的函数介绍的结构
<div class="func-info" v-else>
<div class="func-info-info">
<span class="func-name">
{{ funcInfo.name }}
</span>
<span>
{{ funcInfo.info }}
</span>
</div>
<div class="usage">
<div class="example-label">
用法:
</div>
<div class="func-info-content">
<span class="func-name">
{{ funcInfo.name }}
</span>
<span>
{{ funcInfo.usage }}
</span>
</div>
</div>
<div class="example">
<div class="example-label">
示例:
</div>
<div class="func-info-content">
<span class="func-name">
{{ funcInfo.name }}
</span>
<span>
{{ funcInfo.example.start }}
</span>
<span v-if="funcInfo.example.content" v-for="(e,i) of funcInfo.example.content" :key="i">
<span class="func-tag">
{{ e }}
</span>
<span v-if="i!=funcInfo.example.content.length-1">
,
</span>
</span>
<span v-if="funcInfo.example.end">
{{ funcInfo.example.end }}
</span>
</div>
</div>
</div>
这里补充一下,前面编辑好的函数会赋值给当前组件的func属性
接下来我们就可以进行函数计算部分的开发了
//这里我们拿一段代码举例,上图是这段代码在页面的显示效果
AVERAGE([[form-number2714ef0fce-计数器]],[[form-selectfe3adeedcf-下拉单选]],[[form-textarea70eb43e02e-多行输入]])
//可以看到 `[[]]` 里面的内容存放的是页面组件对于的id,所以我们的首先将`[[]]` 以及里面的部分替换为对应的id
// 将公式中的id替换为对应的数据
//这里的方法是找的组件列表同时找到函数当中对应的id替换为对应的value
setFunc(array, formula) {
const variables = formula.match(/[[(.*?)]]/g);
if (!variables) {
let keys = Object.keys(funcs)
const regex = new RegExp(`\b(${keys.join("|")})\b`, "g");
// 使用 replace() 方法替换函数名
return formula.replace(regex, "this.$1");
}
for (let i = 0; i < variables.length; i++) {
let ids = variables[i].slice(2, -2).split('-');
const variable = ids[0] + '-' + ids[1];
const item = findItemById(array, variable);
if (item) {
formula = formula.replace(variables[i], String(item.value || ''));
}
}
// 特殊处理部分函数的参数需要字符串格式
let fun = ['LOWER', 'UPPER', 'TRIM', 'TEXT', 'CONCATENATE', 'EXACT'];
formula = formula.replace(/(\w+)((.*?))/g, function (match, functionName, params) {
let arr = params.split(',');
for (let i = 0; i < arr.length; i++) {
if (fun.includes(functionName)) {
arr[i] = `'${arr[i].trim()}'`;
}
}
return `${functionName}(${arr.join(", ")})`;
});
formula = formula.replace(/(\w+)(/g, 'this.$1(');
return formula;
function findItemById(array, id) {
for (let i = 0; i < array.length; i++) {
const item = array[i];
if (item.id.includes(id)) {
return item;
}
if (item.children) {
const childItem = findItemById(item.children, id);
if (childItem) {
return childItem;
}
}
}
return null;
}
}
//处理后的结果
this.AVERAGE(111,222,333)
//处理成这样就可以使用eval进行函数执行了
let v = eval(code); //v = 222
最后粘贴我的函数执行代码
//pd 组件列表是一个tree结构
//d 当前出发input事件的组件 这里的逻辑是当组件的值被修改的时候就执行这个函数,如果当前被修改的组件的id存在于某一个组件的函数也就是func属性中,就会调用此组件的函数
//此函数在页面加载时也会运行一次
funFunc(pd, d) {
let keys = Object.keys(funcs)
pd?.forEach(e => {
if (e.func) {
let code = this.setFunc(this.of.d.children, e.func)
console.log('处理前的函数:', e.func, '处理后的函数:', code)
try {
let v = eval(code);
this.$set(e, 'value', v)
} catch (err) {
//这里处理部分特殊情况
let flag = keys.some(k => {
return code.includes(k)
})
if (!flag) {
console.log('code', code)
this.$set(e, 'value', code)
}
console.log('函数调用错误:', err)
}
}
if (e.children?.length) {
this.seekId(e.children, d)
}
})
}
//最后我们需要将前面的funs.js文件引入我们的vue组件中,在methods中...func解构,就可以了