【阿里低代码引擎实战】— 保存为区块功能

【阿里低代码引擎实战】--- 保存为区块功能

上一篇在低代码项目中实现了表单编辑与回显功能,上篇指路:juejin.cn/post/735238...

这篇我们来实现将页面多个组件组合成一个区块,保存后可以作为新的组件重复拖拽使用的功能;

本篇完整代码github地址: github.com/JoeXu727/al...

github.com/JoeXu727/lo...

一. 在操作辅助区添加"保存为区块"

  1. 期望达到的效果是在操作辅助区多一个+号按钮,点击后会出现弹框,输入一些区块的基本信息后即可保存为区块,如下图;

  2. 前端关键代码实现:在src下新建 actions/block.tsx:

typescript 复制代码
    src/actions/block.tsx
    ​
    import * as React from 'react';
    import { default as html2canvas } from 'html2canvas';
    import { Node } from '@alilc/lowcode-engine';
    import { Dialog, Form, Input } from '@alifd/next';
    import { createBlock } from '../services/mockService';
    ​
    import './index.scss';
    ​
    const FormItem = Form.Item;
    ​
    interface SaveAsBlockProps {
        node: Node;
    }
    ​
    const SaveAsBlock = (props: SaveAsBlockProps) => {
        const { node } = props;
        const [src, setSrc] = React.useState();
        React.useEffect(() => {
            const generateImage = async () => {
                let dom2 = node.getDOMNode();
                const canvas = await html2canvas?.(dom2, { scale: 0.5 });
                const dataUrl = canvas.toDataURL();
                setSrc(dataUrl);
            };
    ​
            generateImage();
        }, []);
    ​
        const save = async (values) => {
            const { name, title } = values;
            const { schema } = node;
    ​
            if (!name || !title) {
                return;
            }
    ​
            await createBlock({
                schemaId: schema.id,
                name,
                title,
                schema: JSON.stringify(schema),
                screenshot: src,
            })
        }
    ​
        return <div style={{ width: '450px' }}>
            <Form colon>
                <FormItem
                    name="name"
                    label="名称"
                    labelCol={{ span: 4 }}
                    wrapperCol={{ span: 16 }}
                    required
                    requiredMessage='please input name'>
                    <Input />
                </FormItem>
                <FormItem
                    name="title"
                    label="简介"
                    labelCol={{ span: 4 }}
                    wrapperCol={{ span: 16 }}
                    required
                    requiredMessage='please input title'>
                    <Input />
                </FormItem>
                <FormItem
                    name="screenshot"
                    label="缩略图"
                >
                    <div className='block-screenshot'>
                        <img src={src} />
                    </div>
                    <Input value={src} style={{ display: 'none' }} />
                </FormItem>
                <FormItem
                    label="" colon={false}>
                    <Form.Submit
                        type="primary"
                        validate
                        onClick={save}
                        style={{ marginLeft: 26 }}>
                        保存
                    </Form.Submit>
                </FormItem>
    ​
            </Form>
        </div>
    }
    ​
    export default {
        name: 'SaveAsBlock',
        content: {
            icon: {
                type: 'add',
                size: 'xs'
            },
            title: '保存为区块',
            action(node: Node) {
                Dialog.show({
                    v2: true,
                    title: '是否保存为区块?',
                    content: <SaveAsBlock node={node} />,
                    footer: false
                })
    ​
            }
        },
        important: true,
    }

<!---->

    src/services/mockService.ts
    ​
    export const createBlock = async (block: any) => {
      const url = `http://127.0.0.1:7001/block`;
    ​
      const res = await (await fetch(url, {
        method: 'post',
        headers: {
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({ block })
      })).json()
    ​
      if (res.code) {
        console.error('create block failed: ', res);
        return;
      }
      Message.success(res.message);
    }
  1. 后端关键代码实现:

    ini 复制代码
    router.js
    ​
    module.exports = app => {
      const { router, controller } = app;
      router.get('/block', controller.block.listBlocks);
      router.post('/block', controller.block.createBlock);
    };
    javascript 复制代码
    controller/block.js
    ​
    'use strict';
    ​
    const Controller = require('egg').Controller;
    ​
    class BlockController extends Controller {
    ​
        async listBlocks() {
            const { ctx } = this;
            const blocks = await ctx.service.block.listBlock();
            ctx.body = {
                code: 0,
                data: blocks,
            };
        }
    ​
        // 新增or更新区块
        async createBlock() {
            const { ctx } = this;
            const { block } = ctx.request.body;
            const result = await ctx.service.block.createOrUpdateBlock(block);
            ctx.logger.info('create block result: ', result);
            ctx.body = {
                code: 0,
                message: '保存区块成功',
            };
        }
    }
    ​
    module.exports = BlockController;
    javascript 复制代码
    service/block.js
    ​
    'use strict';
    ​
    const { Service } = require('egg');
    ​
    class BlockService extends Service {
    ​
        // 查询区块列表
        async listBlock() {
            try {
                return this.app.mysql.select('block');
            } catch (error) {
                console.log(error);
            }
        }
    ​
        // 新增or更新区块
        async createOrUpdateBlock(params) {
            try {
                const query = await this.app.mysql.get('block', { schemaId: params.schemaId });
                return query
                    ? await this.app.mysql.update('block', params, { where: { schemaId: params.schemaId } })
                    : await this.app.mysql.insert('block', params)
            } catch (error) {
                console.log(error);
            }
        }
    }
    ​
    module.exports = BlockService;

二. 数据库操作

  1. 新建 block 表,表结构如下图,其中 schemaId 取得值是 区块schema里面的 id 字段:

三. 新建区块插件

  1. 在 plugins 下新增 plugin-blocks 插件:

    typescript 复制代码
    plugins/plugin-blocks/index.tsx
    ​
    import * as React from 'react';
    import { IPublicModelPluginContext } from '@alilc/lowcode-types';
    ​
    import { default as BlockPane } from './pane';
    import { listBlocks } from '../../apis/block';
    ​
    import './index.scss';
    ​
    const LowcodePluginPluginBlocks = (ctx: IPublicModelPluginContext) => {
        return {
            // 插件对外暴露的数据和方法
            exports() {
                return {
                    data: '你可以把插件的数据这样对外暴露',
                    func: () => {
                        console.log('方法也是一样');
                    },
                };
            },
            // 插件的初始化函数,在引擎初始化之后会立刻调用
            async init() {
                // 你可以拿到其他插件暴露的方法和属性
                // const { data, func } = ctx.plugins.pluginA;
                // func();
    ​
                // console.log(options.name);
    ​
                let config: any = {
                    area: 'leftArea',
                    name: 'LowcodePluginPluginBlocksPane',
                    type: 'PanelDock',
                    props: {
                        icon: 'smile',
                        description: '区块',
                    },
                    contentProps: {
                        api: {
                            listBlocks
                        }
                    },
                    content: BlockPane,
                };
                // 往引擎增加面板
                ctx.skeleton.add(config);
    ​
                ctx.logger.log('打个日志');
            },
        };
    };
    ​
    // 插件名,注册环境下唯一
    LowcodePluginPluginBlocks.pluginName = 'LowcodePluginPluginBlocks';
    LowcodePluginPluginBlocks.meta = {
        // 依赖的插件(插件名数组)
        dependencies: [],
        engines: {
            lowcodeEngine: '^1.0.0', // 插件需要配合 ^1.0.0 的引擎才可运行
        },
    };
    ​
    export default LowcodePluginPluginBlocks;

    记得在 src/index.ts 注册一下:

    javascript 复制代码
    import BlocksPlugin from './plugins/plugin-blocks';
    import saveAsBlock from './actions/block';
    ​
    // 注册保存为区块工作条
    material.addBuiltinComponentAction(saveAsBlock);
    ​
    await plugins.register(BlocksPlugin);

四. 在区块插件面板展示已保存的区块

  1. 效果如下图,支持拖拽到右侧显示:

  2. 关键代码:

    typescript 复制代码
    plugin-blocks/card/index.tsx
    ​
    import * as React from 'react';
    ​
    import './index.scss';
    ​
    const { useState, useEffect } = React;
    ​
    interface BlockCardProps {
        id: string,
        name: string,
        screenshot: string,
    }
    ​
    const BlockCard = (props: BlockCardProps) => {
        const { id, name, screenshot } = props;
    ​
        return <div className='block-card snippets' data-id={id}>
            <div className='block-card-screenshot'>
                <img src={screenshot} />
            </div>
            <span>
                {name}
            </span>
        </div>;
    }
    ​
    export default BlockCard;
    typescript 复制代码
    plugin-blocks/pane/index.tsx
    ​
    import * as React from 'react';
    import { common, project, config, event } from '@alilc/lowcode-engine';
    ​
    import { Loading, Box, Divider } from '@alifd/next';
    ​
    import { default as BlockCard } from '../card';
    import { default as store } from '../store';
    ​
    const { useState, useEffect } = React;
    ​
    export interface Block {
    ​
    }
    ​
    export interface BlockResponse {
        code: number;
        data: Block[];
    }
    ​
    export interface BlockPaneAPI {
        listBlocks: () => BlockResponse;
    }
    ​
    export interface BlockPaneProps {
        api: BlockPaneAPI;
    }
    ​
    export const BlockPane = (props: BlockPaneProps) => {
    ​
        const { api } = props;
        const [blocks, setBlocks] = useState();
        const { listBlocks } = api;
    ​
        useEffect(() => {
            const fetchBlocks = async () => {
                const res = await listBlocks();
                if (res?.code) {
                    console.error('list block failed: ', res);
                    return;
                }
                store.init(res);
                setBlocks(res);
            };
    ​
            fetchBlocks();
        }, []);
    ​
        // 拖拽逻辑
        const registerAdditive = (shell: HTMLDivElement | null) => {
            if (!shell || shell.dataset.registered) {
                return;
            }
    ​
            function getSnippetId(elem: any) {
                if (!elem) {
                    return null;
                }
                while (shell !== elem) {
                    if (elem.classList.contains('snippets')) {
                        return elem.dataset.id;
                    }
                    elem = elem.parentNode;
                }
                return null;
            }
    ​
            const _dragon = common.designerCabin.dragon
            if (!_dragon) {
                return;
            }
    ​
            // eslint-disable-next-line
            const click = (e: Event) => { };
    ​
            shell.addEventListener('click', click);
    ​
            _dragon.from(shell, (e: Event) => {
                const doc = project.getCurrentDocument();
                const id = getSnippetId(e.target);
                if (!doc || !id) {
                    return false;
                }
    ​
                const dragTarget = {
                    type: 'nodedata',
                    data: store.get(id),
                };
    ​
                return dragTarget;
            });
    ​
            shell.dataset.registered = 'true';
        };
    ​
        if (!blocks?.length) {
            return <div className='block-pane-loading'><Loading /></div>
        }
    ​
        return <div className='block-pane' ref={registerAdditive}><Box direction='row' wrap>
            {
                blocks.map(item => <BlockCard id={item.id} name={item.name} screenshot={item.screenshot} />)
            }
        </Box>
        </div>;
    ​
    }
    ​
    export default BlockPane;
    javascript 复制代码
    plugin-blocks/store/index.ts
    ​
    class BlockStore {
    ​
        store;
    ​
        constructor() {
            this.store = new Map();
        }
    ​
        init(blocks) {
            blocks.forEach(block => {
                const { id, schema } = block;
                this.store.set(`${id}`, JSON.parse(schema));
            });
        }
    ​
        set(id, snippets) {
            this.store.set(id, snippets);
        }
    ​
        get(id) {
            return this.store.get(id);
        }
    ​
    }
    ​
    const singleton = new BlockStore();
    ​
    export default singleton;

结果演示

新增一个区块666,查看效果:

================================================================= 以上为个人工作学习笔记总结,供学习参考交流,未经允许禁止转载或商用。

本篇参考金蝉大佬:www.bilibili.com/video/BV1YF...

github.com/alibaba/low...

相关推荐
Momo__1 小时前
VueUse createReusableTemplate —— 单文件组件内的模板复用神器
前端·vue.js
程序员小富1 小时前
我开源了一个开发者专属的智能 JSON 工具,得到了媳妇高度认可
前端·vue.js·后端
小小小小宇1 小时前
程序员如何给 LLM 装工具以及看懂推理过程
前端
写代码的皮筏艇1 小时前
React中的forwardRef
前端·react.js·面试
槑有老呆1 小时前
花三个月工资请了个 AI 程序员,结果它连青岛啤酒股价都查不了
前端
风骏时光牛马1 小时前
Verilog开发常见问题汇总解析
前端
子兮曰1 小时前
AI Coding Method Map:一张图看懂 AI 编程的完整链路
前端·人工智能·后端
weedsfly2 小时前
语法糖褪去之后——Babel 转译产物中的 JavaScript 本貌
前端·javascript
JustHappy2 小时前
「软件设计思想杂谈🤔」“切图仔”也能懂编译原理?框架源码也许没那么难。聊聊 Vue 的编译(上)
前端·javascript·vue.js