一. 介绍
上一节介绍了如何构建一个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怎么使用,大家移步官网看一下:
在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生成对应的配置项已经完成,接下来继续实现别的。有兴趣的同学可以点一波关注哦!!!感谢🙏
参考文章:
往期文章: