Low-Code低代码平台技术要点分析 | 青训营笔记

455 阅读12分钟

一、前言

项目概述:实现一个支持采用图形化拖拽的方式,配置参数来生成JSON,通过生成的JSON来渲染出可JSONSchema描述的页面。
Github 地址:github.com/Zilong-417/…

接下来将对以下技术要点进行核心代码分析:

  1. 编辑器
  2. 拖拽
  3. 标线
  4. 撤销与还原
  5. 删除选中与清空画布
  6. 放大与缩小
  7. 预览与导出
  8. 组件属性设置
  9. 组件自定义事件

二、核心代码介绍

1、编辑器

(1)整体布局

编辑器页面效果图如下:

1661413900835.png 编辑器页面采用的是三栏布局,左右固定宽度,中间自适应;

(2)编辑区域实现

中间的画布是编辑区域。它的作用是:从左边物料区拖拽出一个组件放到画布中时,画布要把这个组件渲染出来。
实现思路:

  1. 创建一个data.json文件用于存放容器和组件json数据
  2. 拖拽到画布中时,使用 Es6解构赋值方法将新的组件数据添加到 blocks数组对象中
  3. 编辑器使用 map 方法遍历 blocks数组对象,将每个组件逐个渲染到画布中

核心代码:

(1)容器与组件初始json数据:

{
    "container": {
        "width": 900,
        "height": 650
    },
    "blocks": []
}

(2)单组件构成:

registerConfig.register({
    lable: '按钮',
    preview: () => <ElButton>预览按钮</ElButton>,
    resize: {
        width: true,
        height: true
    },
    render: ({ props, size }) => <ElButton style={{ height: size.height + 'px', width: size.width + 'px', fontSize: props.fontsize + 'px' }} type={props.type} size={props.size}>{props.text || '按钮'}</ElButton>,
    key: 'button',
    props: {
        text: createInputProp('按钮内容'),
        fontsize: createInputProp('字体大小/px'),
        type: createSelectProp('按钮类型', [
            { label: '基础', value: 'primary' },
            { label: '成功', value: 'success' },
            { label: '警告', value: 'warning' },
            { label: '危险', value: 'danger' },
            { label: '文本', value: 'text' },
        ]),
        size: createSelectProp('按钮尺寸', [
            { label: '默认', value: 'default' },
            { label: '大', value: 'large' },
            { label: '小', value: 'small' }
        ])
    }
})

(3)添加组件(解构赋值方法):

const drop = (e) => {
        let blocks = data.value.blocks;
        data.value = {
            ...data.value, blocks: [
                ...blocks, {
                    top: e.offsetY,
                    left: e.offsetX,
                    zIndex: 1,
                    key: currentComponent.key,
                    alignCenter: true,
                    props: {},
                    model: {},
                    events: {}
                }
            ]
        }
    }

(4)渲染组件数据:(map方法)

(data.value.blocks.map((block, index) => (
       <editorBlock
               class={
                         [block.focus ? 'editor-block-focus' : '',
                         previewRef.value ? 'editor-block-preview' : '']}
                         block={block}
                         onMousedown={(e) => blockMousedown(e, block, index)}
                         formData={props.formData}>
                     }
       </editorBlock>
)))

2、拖拽

(1)首先考虑四个状态

  1. dragenter进入组件中添加一个移动的标志
  2. dragover在目标组件经过要阻止默认行为 否则不能触发drop
  3. dragleave离开组件的时候 需要增加一个禁用标识
  4. drop松手时根据拖拽的组件,添加一个组件

核心代码:

    const dragenter = (e) => {
        e.dataTransfer.dropEffect = 'move'
    }
    const dragover = (e) => {
        e.preventDefault()
    }
    const dragleave = (e) => {
        e.dataTransfer.dropEffect = 'none'
    }
    const drop = (e) => {
        let blocks = data.value.blocks;
        data.value = {
            ...data.value, blocks: [
                ...blocks, {
                    top: e.offsetY,
                    left: e.offsetX,
                    zIndex: 1,
                    key: currentComponent.key,
                    alignCenter: true,
                    props: {},
                    model: {},
                    events: {}
                }
            ]
        }
    }

(2)其次组件到画布过程

要想实现组件拖拽就得用h5属性,给它添加一个 draggable 属性。另外,在将组件列表中的组件拖拽到画布中,还有两个事件是起到关键作用的:

  1. dragstart 事件,事件内绑定目标元素的拖拽事件(通过addEventListener)
  2. dragend 事件,在拖拽结束时触发。主要用于移除行为(通过removeEventListener)。

左侧物料区组件列表的代码:
这里使用的map方法遍历数组componentList,渲染出组件列表

{config.componentList.map(component => (
                        <div class="editor-left-item"
                            draggable
                            onDragstart={e => dragstart(e, component)}
                            onDragend={dragend} >
                            <span>{component.lable}</span>
                            <div >{component.preview()}</div>
                        </div>
                    ))}

下面是dragstart和dragend事件的代码:

 const dragstart = (e, component) => {
        //dragenter进入元素中 添加一个移动的标志
        //dragover 在目标元素经过 必须要阻止默认行为 否则不能触发drop
        //dragleave 离开元素的时候 需要增加一个禁用标识
        //drop 松手时 根据拖拽的组件 ,添加一个组件
        containerRef.value.addEventListener('dragenter', dragenter)
        containerRef.value.addEventListener('dragover', dragover)
        containerRef.value.addEventListener('dragleave', dragleave)
        containerRef.value.addEventListener('drop', drop)
        currentComponent = component
    }
    const dragend = (e) => {
        containerRef.value.removeEventListener('dragenter', dragenter)
        containerRef.value.removeEventListener('dragover', dragover)
        containerRef.value.removeEventListener('dragleave', dragleave)
        containerRef.value.removeEventListener('drop', drop)
    }

(3)最后实现组件的移动

要想实现组件移动,在样式方面首先需要将画布设为相对定位 position: relative,然后将每个组件设为绝对定位 position: absolute。除此之外,还要通过监听三个事件来进行移动:

  1. mousedown 事件,在组件上按下鼠标时,记录组件当前的位置,即 xy 坐标
  2. mousemove 事件,每次鼠标移动时,都用当前最新的 xy 坐标减去最开始的 xy 坐标,从而计算出移动距离,再改变组件位置。
  3. mouseup 事件,鼠标抬起时结束移动。

关键代码
mousedown 事件:

// 按下鼠标,获取初始值和选中的位置
    const mousedown = (e) => {
        const { width: BWidth, height: BHeight } = lastSelectBlock.value//最后选中的组件
        dragState = {
            startX: e.clientX,
            startY: e.clientY, // 记录每一个选中的位置
            startLeft: lastSelectBlock.value.left, // 拖拽前的位置 left(即x坐标)
            startTop: lastSelectBlock.value.top,// 拖拽前的位置 top(即y坐标)
            dragging: false,//不是正在拖拽
            startPos: focusData.value.focus.map(({ top, left }) => ({ top, left })),
        document.addEventListener('mousemove', mousemove)
        document.addEventListener('mouseup', mouseup)
    }

mousemove事件: 

const mousemove = (e) => {
        let { clientX: moveX, clientY: moveY } = e
        // 鼠标移动后 - 鼠标移动前 + left、
        let durX = moveX - dragState.startX
        let durY = moveY - dragState.startY
        focusData.value.focus.forEach((block, idx) => {
            block.top = dragState.startPos[idx].top + durY
            block.left = dragState.startPos[idx].left + durX
        })
    }

mouseup 事件:

 const mouseup = (e) => {
        document.removeEventListener('mousemove', mousemove)
        document.removeEventListener('mouseup', mouseup)
        if (dragState.dragging) { // 如果只是点击就不会触发
            events.emit('end');
        }
    }

(4)PS:多组件移动

需求:按住shift键+鼠标左键可实现多选组件,从而实现多组件移动
实现思路

  1. blockMousedown事件:点击某组件时:若按了shift健,同时选中某组件;若没有,则取消其他选中,并选中某组件
  2. clearBlockFocus事件:点击画布时,清空所有选中状态
const selectIndex = ref(-1)//表示没有任何一个被选中

    //最后选择的那一个组件
    const lastSelectBlock = computed(() => data.value.blocks[selectIndex.value])
    const focusData = computed(() => {
        let focus = []
        let unfocused = []
        data.value.blocks.forEach(block => (block.focus ? focus : unfocused).push(block))
        return { focus, unfocused }
    })
    const clearBlockFocus = () => {
        data.value.blocks.forEach(block => block.focus = false)
    }
    //实现拖拽多个元素
    const containerMousedown = () => {
        if (previewRef.value) return;
        clearBlockFocus()
        selectIndex.value = -1
    }
    const blockMousedown = (e, block, index) => {
        if (previewRef.value) return;
        e.preventDefault()
        e.stopPropagation()
        //block上我们规划一个属性focus获取焦点后就将focus变成true
        if (e.shiftKey) {
            if (focusData.value.focus.length <= 1) {
                block.focus = true//当前只有一个节点被选中时 摁住shift键也不会focus状态
            } else {
                block.focus = !block.focus
            }
        } else {
            if (!block.focus) {
                clearBlockFocus()
                block.focus = true//要清空其他的focus属性
            } //当自己已经被选中了,再次点击时还是选中状态
        }
        selectIndex.value = index
    }

3、标线

标线的用处在于确定两组件之间的对齐方式,两组件间对齐方式有10种

  1. 上下方向的两个组件底对顶、顶对顶、中间对中间、底对底、顶对顶会出现横线
  2. 左右方向的两个组件右对左、左对左、中间对中间、右对右、左对右对齐时会出现竖线

实现思路:

(1)获取按下鼠标的初始值和选中的位置

const mousedown = (e) => {
        const { width: BWidth, height: BHeight } = lastSelectBlock.value
        dragState = {
            startX: e.clientX,
            startY: e.clientY, // 记录每一个选中的位置
            startLeft: lastSelectBlock.value.left, // b点拖拽前的位置 (x轴)
            startTop: lastSelectBlock.value.top, // b点拖拽前的位置 (y轴)
            dragging: false,//不是在拖拽
            startPos: focusData.value.focus.map(({ top, left }) => ({ top, left })),
            lines: (() => {
                const { unfocused } = focusData.value // 获取其他没有选中的以及它们的位置坐辅助线
                let lines = { // 计算横线的位置用y来存放 x存放的是纵向
                    x: [],
                    y: []
                };
                [...unfocused,
                {
                    top: 0,
                    left: 0,
                    width: data.value.container.width,
                    height: data.value.container.height
                }].forEach((block) => {
                    const { top: ATop, left: ALeft, width: AWidth, height: AHeight } = block;
                    // 当此元素拖拽到和A元素top一致的时候,要显示这根辅助线,辅助线的位置就是ATop
                    lines.y.push({ showTop: ATop, top: ATop })
                    lines.y.push({ showTop: ATop, top: ATop - BHeight }) // 顶对底
                    lines.y.push({ showTop: ATop + AHeight / 2, top: ATop + AHeight / 2 - BHeight / 2 }) // 中对中
                    lines.y.push({ showTop: ATop + AHeight, top: ATop + AHeight }) // 底对顶
                    lines.y.push({ showTop: ATop + AHeight, top: ATop + AHeight - BHeight }); // 底对底
                    lines.x.push({ showLeft: ALeft, left: ALeft }) // 左对左边
                    lines.x.push({ showLeft: ALeft + AWidth, left: ALeft + AWidth })// 右边对左边
                    lines.x.push({ showLeft: ALeft + AWidth / 2, left: ALeft + AWidth / 2 - BWidth / 2 })
                    lines.x.push({ showLeft: ALeft + AWidth, left: ALeft + AWidth - BWidth })
                    lines.x.push({ showLeft: ALeft, left: ALeft - BWidth }) // 左对右
                });
                return lines
            })()
        }
        document.addEventListener('mousemove', mousemove)
        document.addEventListener('mouseup', mouseup)
    }

(2)计算当前元素与标线数组元素位置

当拖拽B元素到和A元素top(y轴)一致的时候,要显示这根标线,标线的位置就是A Top(y轴),同样的思路,当拖拽B元素到和A元素left(x轴)一致的时候,要显示这根标线,标线的位置就是A left(x轴)

const mousemove = (e) => {
        let { clientX: moveX, clientY: moveY } = e
        if (!dragState.dragging) {
            dragState.dragging = true,
                events.emit('start'); // 触发事件就会记住拖拽前的位置
        }

        // 计算当前元素最新的left和top去线里面找,找到显示线
        // 鼠标移动后 - 鼠标移动前 + left
        let left = moveX - dragState.startX + dragState.startLeft
        let top = moveY - dragState.startY + dragState.startTop

        // 先计算横线 距离参照物元素还有5px的时候,显示
        let y = null
        let x = null
        for (let i = 0; i < dragState.lines.y.length; i++) {
            const { top: t, showTop: s } = dragState.lines.y[i]
            if (Math.abs(t - top) < 5) { // 小于5说明接近了
                y = s // 线要实现的位置
                moveY = dragState.startY - dragState.startTop + t // 容器距离顶部的距离 + 目标的高度 就是最新的moveY
                // 实现快速和这个元素贴在一起
                break // 找到一根线后就跳出循环
            }
        }
        for (let i = 0; i < dragState.lines.x.length; i++) {
            const { left: l, showLeft: s } = dragState.lines.x[i]
            if (Math.abs(l - left) < 5) { // 小于5说明接近了
                x = s // 线要实现的位置
                moveX = dragState.startX - dragState.startLeft + l // 容器距离顶部的距离 + 目标的高度 就是最新的moveY
                // 实现快速和这个元素贴在一起
                break // 找到一根线后就跳出循环
            }
        }

        markLine.x = x // markLine 是一个响应式数据 x, y更新会导致视图更新
        markLine.y = y
        let durX = moveX - dragState.startX
        let durY = moveY - dragState.startY
        focusData.value.focus.forEach((block, idx) => {
            block.top = dragState.startPos[idx].top + durY
            block.left = dragState.startPos[idx].left + durX
        })
    }

(3)页面渲染出标线

{markLine.x !== null && <div class="line-x" style={{ left: markLine.x + 'px' }}></div>}
{markLine.y !== null && <div class="line-y" style={{ top: markLine.y + 'px' }}></div>}

4、撤销与还原

(1)设置保存命令数组与索引

current为索引值,当撤销时索引值减1,当索引为-1时即为有撤销值
queue数组存放操作命令(删除、清空画布。。。。)
commands对象制作命令和执行功能一个映射表
commandArray数组存放所有的命令
destroyArray数组销毁命令

const state = { 
        current: -1, // 前进后退的索引值
        queue: [], //  存放所有的操作命令
        commands: {}, // 制作命令和执行功能一个映射表  
        commandArray: [], // 存放所有的命令
        destroyArray: [],//销毁
}

(2)撤销与还原操作

撤销:假设现在  queue 保存了操作为 [a, b, c, d],当前画布数据是 d,进行撤销后, current索引 -1,现在画布的数据是 c。
还原:还原则相反, current索引加 1,然后将对应的数据赋值给画布

 const registry = (command) => {
        state.commandArray.push(command);
        state.commands[command.name] = (...args) => { // 命令名字对应执行函数
            const { redo, undo } = command.execute(...args);
            redo();
            if (!command.pushQueue) { // 不需要放到队列中直接跳过即可
                return
            }
            let { queue, current } = state;
            if (queue.length > 0) {
                queue = queue.slice(0, current + 1); // 可能在放置的过程中有撤销操作,所以根据当前最新的current值来计算新的对了
                state.queue = queue;
            }
            queue.push({ redo, undo }); // 保存指令的前进后退
            state.current = current + 1;
            console.log(queue);
        }
    }
    registry({//还原操作
        name: 'redo',
        keyboard: 'ctrl+y',
        execute() {
            return {
                redo() {
                    if (state.current == -1 || state.current == state.queue.length - 1) {
                        ElMessage({
                            showClose: true,
                            message: '没有可以还原的内容了!',
                            type: 'warning',
                        })
                        return
                    }; // 没有可以还原的了
                    let item = state.queue[state.current + 1] // 找到当前的下一步还原操作
                    if (item) {
                        item.redo && item.redo()
                        state.current++
                    }
                }
            }
        }
    })
    registry({//撤销操作
        name: 'undo',
        keyboard: 'ctrl+z',
        execute() {
            return {
                redo() {
                    if (state.current == -1) {
                        ElMessage({
                            showClose: true,
                            message: '没有可以撤销的内容了!',
                            type: 'warning',
                        })
                        return
                    }; // 没有可以撤销的了
                    let item = state.queue[state.current] // 找到上一步还原
                    if (item) {
                        item.undo && item.undo()// 这里没有操作队列
                        state.current--
                    }
                }
            }
        }
    })
    registry({ // 如果希望将操作放到队列中可以增加一个属性 标识等会操作要放到队列中
        name: 'drag',
        pushQueue: true,
        init() { // 初始化操作 默认就会执行
            this.before = null
            // 监控拖拽开始事件,保存状态
            const start = () => {
                this.before = deepcopy(data.value.blocks)
            }
            // 拖拽之后需要触发对应的指令
            const end = () => {
                state.commands.drag()
            }
            events.on('start', start)
            events.on('end', end)
            return () => {
                events.off('start', start);
                events.off('end', end)
            }
        },
        execute() { // state.commands.drag()
            let before = this.before;
            let after = data.value.blocks // 之后的状态
            return {
                redo() { // 默认一松手 就直接把当前事情做了
                    data.value = { ...data.value, blocks: after }
                },
                undo() { // 前一步的
                    data.value = { ...data.value, blocks: before }
                }
            }
        }
    })

5、删除选中与清空画布

删除选中与清空画布的思路在于设置两个变量:
before:当前画布中的组件
after: 画布中未选中的组件(删除选中下after为unfocused,清空画布下after为[])

registry({// 删除操作
        name: 'delete', // 删除
        pushQueue: true,
        execute() {
            let state = {
                before: deepcopy(data.value.blocks), // 当前的值
                after: focusData.value.unfocused// 选中的都删除了 留下的都是没选中的
            }
            return {
                redo: () => {
                    if (focusData.value.focus.length == 0 && focusData.value.unfocused.length == 0) {
                        ElMessage({
                            showClose: true,
                            message: '没有可以删除的内容了!',
                            type: 'warning',
                        })
                        return
                    } else if (focusData.value.focus.length == 0) {
                        ElMessage({
                            showClose: true,
                            message: '还没选中需要删除的内容呢!',
                            type: 'warning',
                        })
                        return
                    }
                    else {
                        data.value = { ...data.value, blocks: state.after }
                    }
                },
                undo: () => {
                    data.value = { ...data.value, blocks: state.before }
                }
            }
        }
    })
    registry({// 清空画布操作
        name: 'deleteAll', // 删除
        pushQueue: true,
        execute() {
            let state = {
                before: deepcopy(data.value.blocks), // 当前的值
                after: []
            }
            return {
                redo: () => {
                    if (focusData.value.focus.length == 0 && focusData.value.unfocused.length == 0) {
                        ElMessage({
                            showClose: true,
                            message: '没有可以清空的内容了!',
                            type: 'warning',
                        })
                        return
                    }
                    else {
                        data.value = { ...data.value, blocks: state.after }
                    }
                },
                undo: () => {
                    data.value = { ...data.value, blocks: state.before }
                }
            }
        }
    })

6、放大与缩小

当点击画布组件时,组件上会出现 8 个小圆点。点击圆点拖拽可放大与缩小 实现思路

(1)向组件属性添加 对象resize

width: true表示更改组件的横向大小 ,height: true表示更改组件的竖向大小, render函数中形参size接收实时组件大小

registerConfig.register({
    lable: '输入框',
    resize: {
        width: true, // 更改输入框的横向大小
        height: true
    },
    preview: () => <ElInput placeholder="预览输入框" ></ElInput>,
    render: ({ model, props, size }) => <ElInput placeholder={props.text == null ? '请输入内容' : props.text}  {...model.default} style={{ width: size.width + 'px' }}></ElInput>,
    key: 'input',
    model: {
        default: '绑定字段'
    },
    props: {
        text: createInputProp('提示信息'),
    }
})

(2)绘制圆点与设置鼠标按下事件

horizontal: 'start'表示标记圆点的位置,start表示组件左边的圆点,end表示右边的圆点,center表示中间的圆点
vertical: 'center' 表示圆点居中

            {width && <>
                <div class="block-resize block-resize-left"
                    onMousedown={e => onmousedown(e, { horizontal: 'start', vertical: 'center' })}></div>
                <div class="block-resize block-resize-right"
                    onMousedown={e => onmousedown(e, { horizontal: 'end', vertical: 'center' })}></div>
            </>}

            {height && <>
                <div class="block-resize block-resize-top"
                    onMousedown={e => onmousedown(e, { horizontal: 'center', vertical: 'start' })}></div>
                <div class="block-resize block-resize-bottom"
                    onMousedown={e => onmousedown(e, { horizontal: 'center', vertical: 'end' })}></div>
            </>}

            {width && height && <>
                <div class="block-resize block-resize-top-left"
                    onMousedown={e => onmousedown(e, { horizontal: 'start', vertical: 'start' })}></div>
                <div class="block-resize block-resize-top-right"
                    onMousedown={e => onmousedown(e, { horizontal: 'end', vertical: 'start' })}></div>
                <div class="block-resize block-resize-bottom-left"
                    onMousedown={e => onmousedown(e, { horizontal: 'start', vertical: 'end' })}></div>
                <div class="block-resize block-resize-bottom-right"
                    onMousedown={e => onmousedown(e, { horizontal: 'end', vertical: 'end' })}
                ></div>
            </>}

(3)组件放大与缩小

实现思路

  1. 记录点击的坐标 xy。
  2. 如果拖拽的是 中间的点 X轴是不变的,如果是对角点 需要取反,拿到正确的组件top和left
  3. 用新的 坐标减去原来的坐标,就可以知道在横轴或者纵轴方向的移动距离是多少。
  4. 最后再将移动距离加上原来组件的高度,就可以得出新的组件高度。

获取点击组件坐标及宽高数据代码:

 const onmousedown = (e, direction) => {
            e.stopPropagation();
            data = {
                startX: e.clientX,
                startY: e.clientY,
                startWidth: props.block.width,
                startHeight: props.block.height,
                startLeft: props.block.left,
                startTop: props.block.top,
                direction
            }
            document.body.addEventListener('mousemove', onmousemove)
            document.body.addEventListener('mouseup', onmouseup)
}

组件放大与缩小逻辑代码:

        const { width, height } = props.component.resize || {}
        let data = {}
        const onmousemove = (e) => {
            let { clientX, clientY } = e;
            let { startX, startY, startWidth, startHeight, startLeft, startTop, direction } = data;


            if (direction.horizontal == 'center') {  // 如果拖拽的是 中间的点 X轴是不变的
                clientX = startX
            }
            if (direction.vertical == 'center') {  // 只能改横向 纵向是不发生变化的
                clientY = startY
            }

            let durX = clientX - startX;
            let durY = clientY - startY;

            if (direction.vertical == 'start') { // 针对反向拖拽的点 需要取反,拿到正确的组件top和left
                durY = -durY
                props.block.top = startTop - durY
            }
            if (direction.horizontal == 'start') {
                durX = -durX
                props.block.left = startLeft - durX
            }


            const width = startWidth + durX;
            const height = startHeight + durY;

            props.block.width = width;
            props.block.height = height; // 拖拽的时候 改变了宽高
            props.block.hasResize = true

7、预览与导出

(1)预览

实现预览是在编辑功能上实现的,当点击预览时,点击组件不能够获取到焦点
(1)首先设置初始态为编辑态const previewRef = ref(false); (2)当点击预览按钮时,previewRef.value取非并清除全部焦点 clearBlockFocus();

{

    label: '预览', handler: () => {
    previewRef.value = !previewRef.value;
     clearBlockFocus();
    }
},

(3)根据焦点状态渲染页面样式

   class={
   [block.focus ? 'editor-block-focus' : '',
   previewRef.value ? 'editor-block-preview' : ''
   ]}

(2)导出

导出代码的方式为json,通过jsonSchema描述页面 只需要接收到用户给组件的属性,再通过双向绑定的方法呈现在页面即可

const state = reactive({
      option: props.option, // 用户给组件的属性
})
return () => {
            return <ElDialog v-model={state.isShow} title={state.option.title}>
                {{
                    default: () => <ElInput
                        type="textarea"
                        v-model={state.option.content}
                        rows={10}
                    ></ElInput>,
                    footer: () => state.option.footer && <div>
                        <ElButton onClick={onCancel}>取消</ElButton>
                        <ElButton type="primary" onClick={onConfirm}>确定</ElButton>
                    </div>
                }}
            </ElDialog>
        }
{
      label: '导出', handler: () => {
       $dialog({
                title: '导出json使用',
                content: JSON.stringify(data.value),
       })
       }
},

8、组件属性设置

(1)配置属性

我们通过工厂方法,提取函数的公共部分,实现逻辑复用; 在props对象中配置属性,并通过render函数将所需属性配置进标签中。

export let registerConfig = createEditorConfig();
const createInputProp = (label) => ({ type: 'input', label })//工厂方法,复用
const createColorProp = (label) => ({ type: 'color', label })
const createSelectProp = (label, options) => ({ type: 'select', label, options })
const createAddressProp = (label) => ({ type: 'link', label })
const createPictureProp = (label) => ({ type: 'picture', label })
const createFileProp = (label) => ({ type: "file", accept: "video/*", label })
registerConfig.register({
    lable: '按钮',
    preview: () => <ElButton>预览按钮</ElButton>,
    resize: {
        width: true,
        height: true
    },
    render: ({ props, size }) => <ElButton style={{ height: size.height + 'px', width: size.width + 'px', fontSize: props.fontsize + 'px' }} type={props.type} size={props.size}>{props.text || '按钮'}</ElButton>,
    key: 'button',
    props: {
        text: createInputProp('按钮内容'),
        fontsize: createInputProp('字体大小/px'),
        type: createSelectProp('按钮类型', [
            { label: '基础', value: 'primary' },
            { label: '成功', value: 'success' },
            { label: '警告', value: 'warning' },
            { label: '危险', value: 'danger' },
            { label: '文本', value: 'text' },
        ]),
        size: createSelectProp('按钮尺寸', [
            { label: '默认', value: 'default' },
            { label: '大', value: 'large' },
            { label: '小', value: 'small' }
        ])
    }
})

(2)渲染页面属性

先判断鼠标是否点击组件,如果点击画布,则显示容器宽高,否则根据组件配置props遍历所含属性。

if (!props.block) {
                content.push(<>
                    <ElFormItem label="容器宽度">
                        <ElInputNumber controls-position="right" style="width:400px" v-model={state.editData.width}></ElInputNumber>
                    </ElFormItem>
                    <ElFormItem label="容器高度">
                        <ElInputNumber controls-position="right" style="width:400px" v-model={state.editData.height}></ElInputNumber>
                    </ElFormItem>
                </>)
            } else {
                let component = config.componentMap[props.block.key];
                if (component && component.props) {
                    content.push(Object.entries(component.props).map(([propName, propConfig]) => {
                        return <ElFormItem label={propConfig.label}>
                            {{
                                input: () => <ElInput style="width:400px" v-model={state.editData.props[propName]}></ElInput>,
                                color: () => <ElColorPicker v-model={state.editData.props[propName]}></ElColorPicker>,
                                select: () => <ElSelect style="width:400px" v-model={state.editData.props[propName]}>
                                    {propConfig.options.map(opt => {
                                        return <ElOption label={opt.label} value={opt.value}></ElOption>
                                    })}
                                </ElSelect>,
                                link: () =>
                                    <div id="prepend">
                                        <div>Https://</div>
                                        <ElInput v-model={state.editData.props[propName]} style="width:180px"></ElInput>
                                    </div>,
                                picture: () =>
                                    <ElUpload
                                        action="/dev-api/admin/product/fileUpload"
                                        show-file-list={false}
                                        on-success={handleAvatarSuccess}
                                        before-upload={beforeAvatarUpload}
                                        v-model={state.editData.props[propName]}
                                        limit={1}
                                    >
                                        <div class="uploader-box">
                                            <ElImage v-show={state.editData.props['picture']} src={state.editData.props['picture']} class="avatar"></ElImage>
                                            <ElButton type="success" >{!state.editData.props['picture'] ? '点击上传' : '点击替换'}</ElButton>
                                        </div>

                                    </ElUpload>,
                                file: () =>
                                    <div>
                                        <input class="file" type="file" accept="video/*" onchange={updateFace} ></input>
                                        {/* <video class="video" src={AUDIO} controls></video> */}
                                        {/* <button onClick={console.log(AUDIO,'onclick')}>删除视频</button> */}
                                    </div>

                            }[propConfig.type]()}
                        </ElFormItem >

                    }))
                }
                
            }

9、组件自定义事件

(1)定义事件睡醒

每个组件有一个 events 对象,用于存储绑定的事件。

// 自定义事件
const events = {
    redirect(url) {
        if (url) {
            window.open('https://' + url, '_blank');
        }
    },

    alert(msg) {
        if (msg) {
            alert(msg)
        }
    },
}

const eventList = [
    {
        key: 'redirect',
        label: 'url事件',
        title: 'url地址',
        hint: '请输入完整url地址',
        event: events.redirect,
    },
    {
        key: 'alert',
        label: 'alert 事件',
        title: '弹窗内容',
        hint: '请输入内容',
        event: events.alert
    },
]

(2)添加事件

给每一个组件添加点击事件onclick,events.alert, events.redirect为渲染页面传过来的值

 if (
            (events.redirect != undefined && events.alert != undefined) ||
            (events.redirect != undefined && events.alert == undefined) ||
            (events.redirect == undefined && events.alert != undefined)) {
            return <ElButton
                style={{ height: size.height + 'px', width: size.width + 'px', fontSize: props.fontsize + 'px' }}
                type={props.type}
                onClick={() => this.handleClick(events.alert, events.redirect)}
                size={props.size}>{props.text || '按钮'}</ElButton>
        } else {
            return <ElButton style={{ height: size.height + 'px', width: size.width + 'px', fontSize: props.fontsize + 'px' }} type={props.type} size={props.size}>{props.text || '按钮'}</ElButton>
        }

再依次遍历events,将已添加的点击事件一次触发

 handleClick(alert, redirect) {
        // 循环触发绑定的事件
        Object.keys(events).forEach(event => {
            if (event == 'alert') events['alert'](alert)
            if (event == 'redirect') events['redirect'](redirect)
        })
    }