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

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

上一篇在低代码项目中实现了表单编辑与回显功能,上篇指路: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...

相关推荐
ayqy贾杰3 小时前
Claude Fable 5 提示词泄漏,抓紧学习下
前端·后端·面试
To_OC12 小时前
别再串行写 await 了,Promise.all 才是并行请求的正确打开方式
前端·javascript·promise
vipbic12 小时前
中后台越做越乱后,我用插件化把它救回来了
前端·vue.js
Hyyy12 小时前
Computer Use 适合做什么,不适合做什么——一次真实使用后的思考
前端
小和尚同志13 小时前
前端 AI 单元测试思考与落地
前端·人工智能·aigc
invicinble14 小时前
c端系统,其实更像一个信息展示平台
前端
李姆斯15 小时前
管理是否可以被完全量化
前端·产品经理·团队管理
名字还没想好☜15 小时前
Next.js 中间件实战:鉴权、重定向与 A/B 分流
开发语言·前端·javascript·中间件·react·next.js
广州灵眸科技有限公司15 小时前
瑞芯微RV1126B开发板(EASY-EAI-PI2) INI文件操作
java·前端·javascript·网络·人工智能
kaiking_g16 小时前
vue项目中,静态导入 vs 动态导入 详解
前端·javascript·vue.js