如何实现钉钉宜搭的函数编辑计算功能

573 阅读4分钟

如何实现钉钉宜搭的函数编辑计算功能

前言:钉钉宜搭是阿里的低代码平台,函数编辑功能可以获取页面的组件,书写函数,使组件与参数参与计算 本文章参考# CodeMirror 6+ vue3 实现简单的计算公式,插入标签功能

本文章阅读需要有部分低代码使用基础

image.png

拆分功能

  • 函数编辑框:这里使用的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属性

接下来我们就可以进行函数计算部分的开发了

image.png

//这里我们拿一段代码举例,上图是这段代码在页面的显示效果
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解构,就可以了