如何从无到有搭建一套完整的低代码平台(四)页面design设计

一. 介绍

上一节介绍了如何构建一个commonUI提供我们后续使用。通过自己写一个脚本的方式提供我们生成。通过这个基本的UI我们可以实现一下design层的构建。

这期我们继续完成低代码平台的设计和搭建。对之前文章有兴趣的同学,可以去浏览一下,链接放在结尾。

二. desgin页面分析

按照之前的页面布局原子组件 + 布局 + 单组件配置面板

我们要生成一套可以使用的原子组件,通过原子组件的配置来生成在布局上的渲染组件。听起来有点绕口,这里我画了一个图来帮助大家了解。

核心是这几个点

  • 通过commonUI组件生成原子组件。
  • 通过一个settings.json和原子组件生成组件可配置的面板。
  • 把可配置的面板和原子组件一起生成渲染组件在布局上渲染出来。
  • 一些事件的处理和派发。

这期主要是考虑一下原子组件的生成和可配置面板的生成。

三. 组件生成

1. 组件描述文件

考虑一个问题,如何描述一个渲染组件,比如说,一个button,一个input框,我们如何去描述它?这里的描述是指,一个渲染组件,要暴露那些配置项给用户使用,(就好比一个通用组件库,暴露API)。

比如:

这是一个button组件,我们暴露name,text,size等属性可以提供给用户配置使用。那我们需要一个通用的文件或者说是一个准则去描述一下,然后根据这个文件去生成。

ok!回到我们的client的根目录上在src文件夹新建一个settings.json文件

js 复制代码
{
    "button": {
        "name": "button",
        "label": "按钮",
        "description": "a component button",
        "settings": [
            {
                "label": "按钮标题",
                "key": "title",
                "type": "input"
            },
            {
                "label": "按钮名称",
                "key": "name",
                "type": "input"
            },
            {
                "label": "按钮基本样式",
                "key": "style",
                "type": "select",
                "options": [
                    "primary",
                    "success",
                    "info",
                    "warning",
                    "danger"                
                ]
            },
            {
                "label": "尺寸",
                "key": "size",
                "type": "select",
                "options": [
                    "small",
                    "default",
                    "larget"
                ]
            },
            {
                "label": "是否朴素按钮",
                "key": "plain",
                "type": "boolean",
                "defaultValue": false
            },
            {
                "label": "是否圆角",
                "key": "round",
                "type": "boolean",
                "defaultValue": false
            },
            {
                "label": "是否禁用",
                "key": "disabled",
                "type": "boolean",
                "defaultValue": false
            }
        ]
    },
}

解释一下这里面的一些字段的含义。。

  • name: 组件名称 渲染原子组件的时候会用到。

  • label: 组件label 渲染原子组件的时候会用到。

  • description: 对组件的一些基本描述

  • settings: 核心的配置文件。

前面三个基本不变,主要是对settings进行设计。

2. settings

可以看出settings是一个数组对象。为什么是一个数组?因为要循环生成每一个配置项。

配置项里面有几个必须字段:

  • label: 这个组件的标题
  • key: 这个组件当前配置项的值
  • type: 这个配置项是一个什么样的类型

核心是这个type,这个type决定,当前配置项和用户是那种形式的交互。

比如input类型:表示当前是输入,需要用户去输入内容。

js 复制代码
     {
        "label": "按钮名称",
        "key": "name",
        "type": "input"
    },

比如select类型:表示当前是选择,需要用户去选择内容,这里就需要提供一个options, 表示有哪些属性可以选择。

js 复制代码
     {
        "label": "尺寸",
        "key": "size",
        "type": "select",
        "options": [
            "small",
            "default",
            "larget"
        ]
    }

比如boolean类型: 表示当前是单选, 需要给一个defaultValue,默认值。

js 复制代码
    {
        "label": "是否禁用",
        "key": "disabled",
        "type": "boolean",
        "defaultValue": false
    }

大致罗列这几种配置项,之后会专门写个文档解释一下。

三. 页面搭建

1. application

在src下新建application文件夹: 依次新建:

diff 复制代码
- Component
- ComponentSettingsDraw
- ComponentTools
- DrawContainer

这里注意一下,因为要导入json文件, ts要开启一个配置:

js 复制代码
    "resolveJsonModule": true

还有就是vscode禁用vetur,启用一下 Vue Language Features (Volar)

OK,那我们开始。(注意一下,页面样式比较丑,没时间解释了,老司机快上车!!!!!)

在 DrawContainer中新建DrawContainer.vue:

js 复制代码
    // 渲染页面
    <template>
        <div id="draw" class="draw-contain">
            <ComponentTools></ComponentTools>
            <ComponentSettingsDraw></ComponentSettingsDraw>
        </div>
    </template>

    <script lang="ts" setup>
    import ComponentTools from '../ComponentTools/ComponentTools.vue';
    import ComponentSettingsDraw from '../ComponentSettingsDraw/ComponentSettingsDraw.vue';

    </script>


    <style lang="less" scoped>
    .draw-contain {
        width: 100vw;
        height: 100vh;
        display: flex;
        background-color: rgb(242, 242, 242);
    }
    </style>

页面比较简单主要就是引入componentTools组件和ComponentSettingsDraw组件。

核心功能在这两个组件中,那么接下来主要讲一下这两个组件的设计和代码

2. ComponentTools文件

在application/ComponentTools 新建 ComponentTools.vue组件:

js 复制代码
    <template>
        ComponentTools
    </template>
    
     <script lang="ts" setup>
     </script>

这个组件的主要功能有两个:

  • 页面:展示可以选择配置的组件列表
  • 数据:选择之后把对应的组件settings项给可配置组件面板渲染

展示可以选择配置的组件列表

按照之前的配置顺序,我们可以知道,渲染组件的基本配置项都在settings.json文件中。那我们写一个方法,读取settings.json文件,然后通过这个文件,渲染出这个组件列表。

OK,那我们写一个hooks来获取组件列表。

在src下新建: - hooks - panel - useComponentListHooks.ts

这里为了规范hooks,使用use开头的命名规则。

思考一下,这个组件列表需要是一个数组对象,每一项都要是固定的属性。

js 复制代码
    key: (帮助v-for绑定, 可有可无)
    title: ( key)
    name:  组件名称
    type: 组件名称(中文)
    description:  描述

OK, 完成一下这个hooks

js 复制代码
    import { ref, onMounted, Ref } from "vue";
    // 声明的类型
    import { componentlistType } from '@six-membered/types';

    // 定义一下 hooks返回的类型
    export type componentListHooksType = {
        listCount: Ref<number>,
        list: Ref<componentlistType[]>,
        getList: () => void
    }
    
    // 接收整个settings,遍历这个settings,然后一次处理每项放入list中。
    export default function useComponentListHooks(settings: any): componentListHooksType {
        const _settings = settings;
        let listCount = ref<number>(0);
        let list = ref<componentlistType[]>([]);

        const getList = (): void => {
            for (let key in _settings) {
                listCount.value++;
                list.value.push({
                    key: listCount.value,
                    title: key,
                    name: _settings[key].label,
                    type:  _settings[key].name,
                    description: _settings[key].description,
                })
            }
        }
        // mounted阶段执行getList()
        onMounted(() => {
            getList();
        })

        return {
            list,
            listCount,
            getList
        }
    }

新建hooks/index.ts:

js 复制代码
    import useComponentListHooks from './panel/useComponentListHooks';
  
    export {
        useComponentListHooks
    }

这里注意一下@six-membered/types,在第一篇中写道这个types是通用的类型说明,这里就不解释了。结尾会把types代码贴出来。

在 ComponentTools.vue中引入:

js 复制代码
<template>
    <div class="component-tools">
        <div class="component-tools__content">
            <div class="component-tools__content-title">
                <div>添加组件</div>
                <ElButton>关闭</ElButton>
            </div>
            <div>
                <div class="component-title">组件</div>
                <div v-for="item in list" :key="item.key" class="component-list">
                    <div>{{ item.name }} {{ item.type }}</div>
                </div>
            </div>
        </div>
    </div>
</template>

<script lang="ts" setup>
import { watch } from 'vue';
import { ElButton } from '@six-membered/ui';
import settings from '../../settings.json';
import { useComponentListHooks } from '../../hooks';

// 把settings传入,然后解析出来list
const { list } = useComponentListHooks(settings);
</script>

<style lang="less">
.component-tools {
 ...
 // 样式不加了,我的样式不配!!!
}
</style>

OK,现在我们已经从settings.json中读取,并且渲染出来一个可以选择的列表。

对应的组件settings项给可配置组件面板渲染

组件列表已经完成。现在我们需要click这个列表的单项,就可以把当前组件的配置结构可配置组件面板渲染。要考虑一个问题,这个点击选中,有几个作用:

  • 获取current组件的配置结构
  • 维护一个全部渲染组件map(这个就是最后渲染到页面上的map)每次点击,就add一个新的组件。

所以这里需要全局去共享一下数据。这时候,pinia欢迎您!

安装一下pinia:

js 复制代码
    pnpm add pinia

然后在main.ts中引入:

js 复制代码
    import { createPinia } from 'pinia'
    const pinia = createPinia()

    app.use(pinia);

注意:如果出现如下的报错:

这是你安装的pinia版本有问题,需要指定一下版本,我这里用的就是V2.0.35

js 复制代码
    pnpm add pinia@2.0.35   // 大家根据自己情况来定 

具体pinia怎么使用,大家移步官网看一下:

pinia官网

在client/src下 新建store/page/componentStore.ts

这里面先定义三个state:

  • selectComponent: 当前选中的组件(包含:componentType、 id)
  • pageComponentList: 组件渲染列表
  • currentComponentSettings: 当前选中组件的配置项。
js 复制代码
    import { defineStore } from 'pinia';
    import { SelectedComponentType, componentType, IypeSettingsOptions } from '@six-membered/types';
    import { getUidCode } from '@six-membered/tools';


    export const componentStore = defineStore({
        id: 'component',
        state: () => {
            return {
                selectComponent: null as SelectedComponentType | null,
                pageComponentList: [] as SelectedComponentType[],
                currentComponentSettings: [] as IypeSettingsOptions[]
            }
        },

        getters: {
            // 获取当前组件
            getSelectComponent(): SelectedComponentType | null {
                return this.selectComponent
            },
            
            // 获取组件列表
            getPageComponentList(): SelectedComponentType[] {
                return this.pageComponentList
            }
        },

        actions: {
            // 赋值
            setComponent(type: componentType): void {
                this.selectComponent = { id : getUidCode(), componentType: type };
                this.pageComponentList.push(this.selectComponent)
            },

            setCurrentComponentSettings<T>(settingOptions: any): void {
                this.currentComponentSettings = settingOptions;
            }
        }
    })

这个要说一下: 为什么需要一个ID?因为之后会对组件的配置项修改和存储。单一ID作为组件的标识。

这里跟types一样,创建了一个公共方法组件tools。

packages/tools/src/index.ts

js 复制代码
    export const getUidCode = () => {

        let passArrItem: any = [];

        const getNumber = (): string | number => Math.floor(Math.random() * 10); // 0~9的数字
        const getUpLetter = (): string => String.fromCharCode(Math.floor(Math.random() * 26) + 65); // A-Z
        const getLowLetter = (): string => String.fromCharCode(Math.floor(Math.random() * 26) + 97); // a-z

        const passMethodArr = [getNumber, getUpLetter, getLowLetter];
        const getIndex = (): number => Math.floor(Math.random() * 3);
        const getPassItem = () => passMethodArr[getIndex()]();

        Array(5).fill('').forEach(() => {
          passArrItem.push(getPassItem());
        })

        const confirmItem = [getNumber(), getUpLetter(), getLowLetter()];
        passArrItem.push(...confirmItem);
        let dataTime = new Date();

        return `${dataTime.getTime()}_${passArrItem.join('')}`;
    }

ok,基本完成的一个store的创建。现在我们去使用一下:

js 复制代码
<template>
    <div class="component-tools">
        <div class="component-tools__content">
            <div class="component-tools__content-title">
                <div>添加组件</div>
                <ElButton>关闭</ElButton>
            </div>
            <div>
                <div class="component-title">组件</div>
                <div v-for="item in list" :key="item.key" class="component-list" @click="componentStore.setComponent(item.type)">
                    <div>{{ item.name }} {{ item.type }}</div>
                </div>
            </div>
        </div>
    </div>
</template>

<script lang="ts" setup>
import { watch } from 'vue';
import { ElButton } from '@six-membered/ui';
import settings from '../../settings.json';
import { useComponentListHooks } from '../../hooks';
import piniaStore from '../../store';

const { list } = useComponentListHooks(settings);
const componentStore = piniaStore.componentStore();

watch(
    () => componentStore.pageComponentList,
    (newVal, oldVal) => {
        console.log('componentStore.pageComponentList-newVal', newVal);
    },
    {
      deep: true
    }
)

</script>

<style lang="less">

</style>

@click="componentStore.setComponent(item.type)" 在当前组件的click时候,执行pinia里面的方法,通过调用actions,对其中的state赋值处理。

可以看到我们想要的值,都在store里面。这样就完成了基本的数据传递。

3. ComponentSettingsDraw文件

已经完成了组件的列表展示,现在要对单独组件的配置项进行处理。

其实也简单,就是对其对settings配置读取,通过不同的type渲染成不同的结果。

这里我们以button组件为例子:

在ComponentSettingsDraw中新建

  • ComponentSettingsDraw.vue
  • ButtonOptionsDraw.vue
ComponentSettingsDraw.vue
js 复制代码
<template>
    <div class="options">
        <ButtonOptionsDraw v-if="componentStore.selectComponent?.componentType === 'button'" />
    </div>
</template>
<script setup lang="ts">
import ButtonOptionsDraw from './ButtonOptionsDraw.vue';
import settings from '../../settings.json';
import { watch } from 'vue';
import piniaStore from '../../store';


const componentStore = piniaStore.componentStore();

watch(
    () => componentStore.selectComponent,
    (newVal, oldVal) => {
        if (newVal) {
            componentStore.setCurrentComponentSettings(settings[newVal.componentType].settings)
        }
    },
    {
        deep: true
    }
)
</script>

<style lang="less" scoped>
.options {
    width: 260px;
    min-height: 700px;
    overflow-y: auto;
    border: 1px solid rgb(200, 200, 200);
    background-color: rgb(255, 255, 255);
}
</style>

因为componentStore。selectComponent.componentType代表每个组件的类型,如果为button,就显示buttonOptionsDraw.vue。

js 复制代码
<ButtonOptionsDraw v-if="componentStore.selectComponent?.componentType === 'button'" />
 
<LayoutOptionsDraw v-if="componentStore.selectComponent?.componentType === 'layout'" />
  
<TextOptionsDraw v-if="componentStore.selectComponent?.componentType === 'text'" />
...

同时监听一下componentStore.selectComponent,如果newValue有,就去执行一下setCurrentComponentSettings(选中组件的配置项)

打印一下:componentStore.currentComponentSettings

这里面就是button的每个配置项。

ButtonOptionsDraw.vue

这个实现就比较简单了,主要就是遍历componentStore.currentComponentSettings,通过不同的type类型,渲染生成不同的配置项。

这里需要先生成几个常见的配置项。

cd application/Component 新建:

lua 复制代码
OptionsInput / index.vue (渲染type为input类型)
OptionsRadio / index.vue (渲染type为boolean类型)
OptionsSelect / index.vue(渲染type为select类型)
...
OptionsInput
js 复制代码
    <template>
    <div class="input_container">
        <div class="input_container-label">{{ options.label }}</div>
        <ElInput placeholder="请输入" size="larget" style="width: 160px"></ElInput>
    </div>
    </template>

    <script lang="ts" setup>
    import { ElInput } from '@six-membered/ui';

    const props = defineProps({
        options: {
            type: Object,
            default: {}
        }

    })

    </script>

    <style lang="less" scoped>
    </style>

没什么好说的,接受options。通过ElInput实现。

OptionsRadio
js 复制代码
<template>
    <div class="radio_container">
        <div class="radio_container-label">{{ options.label }}</div>
        <el-radio-group v-model="radio">
            <el-radio :label="3">是</el-radio>
            <el-radio :label="6">否</el-radio>
        </el-radio-group>
    </div>
</template>

<script setup lang="ts">
import { ElRadio, ElRadioGroup } from '@six-membered/ui';
import { ref } from 'vue';

const radio = ref('');

const props = defineProps({
    options: {
        type: Object,
        default: {}
    }

})

</script>

<style lang="less" scoped>
</style>
OptionsSelect
js 复制代码
<template>
    <div class="select_container">
        <div class="select_container-label">{{ options.label }}</div>
        <el-select v-model="value" placeholder="Select" size="large" style="width: 160px">
            <el-option
                v-for="item in options.options"
                :key="item"
                :label="item"
                :value="item"
            />
        </el-select>
    </div>
</template>

<script setup lang="ts">
import { ElSelect, ElOption } from '@six-membered/ui';
import { ref } from 'vue';

const value = ref('');

const props = defineProps({
    options: {
        type: Object,
        default: {}
    }

})

</script>

<style lang="less" scoped>
</style>

ok,完成之后,把这三个作为单独的子项,引入ButtonOptionsDraw.vue

回到ButtonOptionsDraw.vue

js 复制代码
<template>
    // 依次遍历,逐个生成。
    <div v-for="(item, index) in componentStore.currentComponentSettings" :key="index">
        <options-input v-if="item.type === 'input'" :options="item"></options-input>
        <options-select v-if="item.type === 'select'" :options="item"></options-select>
        <options-radio v-if="item.type === 'boolean'" :options="item"></options-radio>
    </div>
</template>

<script setup lang="ts">
import piniaStore from '../../store';
import OptionsInput from '../Component/OptionsInput/index.vue';
import OptionsSelect from '../Component/OptionsSelect/index.vue';
import OptionsRadio from '../Component/OptionsRadio/index.vue';

const componentStore = piniaStore.componentStore();

console.log(componentStore.currentComponentSettings);
</script>

<style>
/* div {
    display: flex;
    justify-content: space-around;
    align-items: center;
} */
</style>

现在我们在页面上试一下:

可以看出,当我们点击button后,就根据不同的type渲染出来配置项。

OK!!完成了基本的实现。

四. 结尾

design页面通过settings生成对应的配置项已经完成,接下来继续实现别的。有兴趣的同学可以点一波关注哦!!!感谢🙏

参考文章:

element-plus官网

pinia官网

往期文章:

# 如何从无到有搭建一套完整的低代码平台(一)项目搭建

# 如何从无到有搭建一套完整的低代码平台(二)前端选型

# 如何从无到有搭建一套完整的低代码平台(三)通用组件库的配置

相关推荐
m0_748236582 分钟前
《Web 应用项目开发:从构思到上线的全过程》
服务器·前端·数据库
博客zhu虎康15 分钟前
ElementUI 的 form 表单校验
前端·javascript·elementui
cronaldo9136 分钟前
研发效能DevOps: Vite 使用 Element Plus
vue.js·vue·devops
敲啊敲952743 分钟前
5.npm包
前端·npm·node.js
CodeClimb1 小时前
【华为OD-E卷-木板 100分(python、java、c++、js、c)】
java·javascript·c++·python·华为od
咸鱼翻面儿1 小时前
Javascript异步,这次我真弄懂了!!!
javascript
brrdg_sefg1 小时前
Rust 在前端基建中的使用
前端·rust·状态模式
m0_748230941 小时前
Rust赋能前端: 纯血前端将 Table 导出 Excel
前端·rust·excel
qq_589568102 小时前
Echarts的高级使用,动画,交互api
前端·javascript·echarts