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