最近,接手公司之前的低代码配置项目,项目是用来配置C端活动页的。使用中,发现了很多弊端,问题一大堆,惨不忍睹😂😂😂,正好组内也是有项目迁移的打算,然后就自己动手实现了一个。话不多说,开整😏😏
一、初始化项目
活动配置类的低代码项目,除了配置平台,还有一个承载页展示。我们用lerna来初始化项目,方便管理。
js
mkdir lowCode
cd lowCode
lerna init --independent
mkdir packages
cd packages
新建两个项目low-code-page(C端承载页)、low-code-set(配置平台)。
生成后的目录。
将项目中package.json中的"private": true
去掉,使用lerna list,便能看到lerna下的两个项目了。
1、添加依赖
low-code-set, vant用于中间中间展示配置,vuedraggable
拖拽组件核心
js
npm i vant element-plus @element-plus/icons-vue vue-router@4 vuedraggable less -D
low-code-page
js
npm i vant less -D
2、项目配置
low-code-set 添加配置页面
js
// packages/low-code-set/src/views/pageSet/index.vue
<template>
<div class="content">
<div class="set">
<div class="left">
</div>
<div class="center">
</div>
<div class="right">
</div>
</div>
<div class="bottom">
<button @click="handleSave">保存</button>
<button @click="handlePreview">预览</button>
</div>
</div>
</template>
<script setup lang="ts">
const handleSave = () => {
};
const handlePreview = () => {
};
</script>
<style scoped lang="less">
.content {
display: flex;
flex-direction: column;
height: 100%;
overflow: auto;
background: #efefef;
.left,
.right {
flex: 1;
height: 100%;
overflow: auto;
background: #fff;
padding: 20px;
}
.center {
width:375px; // 设置宽度为普通屏幕标准750/2
height: 100%;
overflow: auto;
background: #fff;
margin: 0 20px;
position: relative;
.render-draggable {
height: 100%;
overflow: auto;
}
}
}
.item {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 80px;
height: 80px;
border: 1px solid #efefef;
}
.left-draggable {
display: flex;
}
.set {
display: flex;
flex: 1;
overflow: auto;
}
.bottom {
text-align: center;
padding: 15px;
button {
padding: 5px 20px;
border-radius: 5px;
cursor: pointer;
&:first-child {
background: #0097ff;
color: #fff;
border: none;
}
&:not(:first-child) {
margin-left: 10px;
border: 1px solid #efefef;
}
}
}
:deep(.select-comp) {
border: 1px dashed #efefef;
}
</style>
其余项目配置不是重点,此处就不阐述了。启动项目后页面如下, 左边是组件摆放区,中间是渲染区,右侧是组件配置区。
low-code-page就很简单了,等会和渲染一起讲。
实现
组件
我们先在packages/low-code-set/src/components目录下实现1个基础组件image。
配置组件信息
js
// packages/low-code-set/src/components/configs.ts
const configs = [
{
groupName: '其他',
components: [
{
//渲染组件名
render: 'Image',
name: '图片',
//组件区图片
icon: 'Picture',
//配置数据
configData: {
style: 'width:100%',
url: 'https://fastly.jsdelivr.net/npm/@vant/assets/cat.jpeg'
}
}
]
},
]
export default configs
创建组件文件,config是右侧配置信息,render是中间渲染,index负责注册组件
js
// config.vue
<template>
<el-form :model="configData" label-width="80px">
<el-form-item label="样式">
<el-input v-model="configData.style" />
</el-form-item>
<el-form-item label="图片链接">
<el-input v-model="configData.url" />
</el-form-item>
</el-form>
</template>
<script setup lang="ts">
import { computed } from "vue";
const props = defineProps<{
data: any;
}>();
const configData = computed(() => props.data);
</script>
js
// render.vue
<template>
<img :src="configData.url" :style="configData.style" />
</template>
<script setup lang="ts">
import { computed } from "vue";
const props = defineProps<{
data: any;
}>();
const configData = computed(() => props.data.configData);
</script>
js
// index.ts
import Config from './config.vue'
import Render from './render.vue'
const install = (app:any) => {
app.component('ImageConfig',Config)
app.component('Image',Render)
}
export default install
使用组件
js
//packages/low-code-set/src/components/index.ts
import Image from './image'
const install = (app: any) => {
app.use(Image)
}
export default install
在packages/low-code-set/src/main.ts中引入使用
diff
import { createApp } from 'vue'
import router from './router'
import App from './App.vue'
import ElementPlus from 'element-plus'
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
+ import Components from './components'
import 'element-plus/dist/index.css'
import './style.css'
import 'vant/lib/index.css';
const app = createApp(App);
-app.use(router).use(ElementPlus).mount('#app')
+app.use(router).use(Components).use(ElementPlus).mount('#app')
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component)
}
有了上面的组件后,就需要实现拖拽了,这也是重点,但不是难点😄😄😄😄。
拖拽
拖拽的实现借助vuedraggable 来实现。
1、根据组件配置渲染左侧组件区。
需要注意的是,draggable一定要配置clone,不然三个数据都是指向同一个地址,会出现改一动全的bug,sort=false,防止组件区的相互拖拽
diff
<template>
<div class="content">
<div class="set">
<div class="left">
+ <div v-for="group in componentConfig" :key="group.groupName">
+ <div>
+ {{ group.groupName }}
+ </div>
+ <draggable
+ :group="{ name: group.groupName, pull: 'clone', put: false }"
+ :list="group.components"
+ animation="300"
+ :sort="false"
+ :clone="clone"
+ @end="handleEnd"
+ class="left-draggable"
+ >
+ <template #item="{ element }">
+ <div class="item">
+ <component :is="element.icon" style="width: 20px" />
+ <span>{{ element.name }}</span>
+ </div>
+ </template>
+ </draggable>
+ </div>
</div>
<div class="center"></div>
<div class="right"></div>
</div>
<div class="bottom">
<button @click="handleSave">保存</button>
<button @click="handlePreview">预览</button>
</div>
</div>
</template>
<script setup lang="ts">
+ import draggable from "vuedraggable";
+ import componentConfig from "@/components/configs";
// 拖拽结束回调
+ const handleEnd = (evt: any) => {
+ console.log("handleEnd==>", evt);
+ };
const handleSave = () => {};
const handlePreview = () => {};
+ const clone = (obj: any) => {
+ // 深拷贝一个对象,否则三个数据指向的都是一个地址
+ const newObj = JSON.parse(JSON.stringify(obj));
+ return newObj;
+};
</script>
......
2、中间接收区
diff
......
<div class="center">
+ <!-- 用于接收拖拽数据 -->
+ <draggable
+ :list="renderList"
+ :group="{ name: 'renderList', pull: true, put: true }"
+ animation="300"
+ class="render-draggable"
+ >
+ <template #item="{ element }">
+ <component
+ :is="element.render"
+ :data="element"
+ :class="{ 'select-comp': currComp === element }"
+ @click="() => handleSelectComponent(element)"
+ ></component>
+ </template>
+ </draggable>
</div>
......
<script setup lang="ts">
+ import { ref } from "vue";
import draggable from "vuedraggable";
import componentConfig from "@/components/configs";
//渲染区数据
+ const renderList = ref([]);
//当前选中设置组件
+ const currComp = ref({});
const handleEnd = (evt: any) => {
console.log("handleEnd==>", evt);
};
+ const handleSelectComponent = (component: any) => {
+ currComp.value = component;
+ };
const handleSave = () => {};
const handlePreview = () => {};
const clone = (obj: any) => {
// 深拷贝一个对象,否则三个数据指向的都是一个地址
const newObj = JSON.parse(JSON.stringify(obj));
return newObj;
};
</script>
......
右侧配置区
右侧配置区就很简单了,直接拿到当前选中的组件,将configData传过去,如下
diff
......
<div class="right">
+ <div>{{ currComp.name }}</div>
+ <component
+ :is="`${currComp.render}Config`"
+ :data="currComp.configData"
+ ></component>
</div>
至此,配置端的核心已经完成了70%😄😄😄😄。
完善
配置中,少不了容器组件Container、图片热点区域组件HotArea。
Container
container/config.vue
js
// container/config.vue
<template>
<el-form :model="configData" label-width="80px">
<el-form-item label="样式">
<el-input v-model="configData.style" />
</el-form-item>
</el-form>
</template>
<script setup lang="ts">
import { computed } from "vue";
const props = defineProps<{
data: any;
}>();
const configData = computed(() => props.data);
</script>
container/render.vue,也是一个draggable组件,可以往里或往外拖组件。通过inject
引用根组件当前的选中组件与组件点击事件。
js
// container/render.vue
<template>
<draggable
:list="list"
:group="{ name: 'renderList', pull: true, put: true }"
animation="300"
:style="configData.style"
>
<template #item="{ element }">
<component
:is="element.render"
:data="element"
:class="{ 'select-comp': currComp === element }"
@click.stop="() => handleSelectComponent(element)"
></component>
</template>
</draggable>
</template>
<script setup lang="ts">
import { computed, ref, inject } from "vue";
import draggable from "vuedraggable";
const currComp = inject("currComp");
const handleSelectComponent = inject("handleSelectComponent");
const props = defineProps<{
data: any;
}>();
const list = computed(() => props.data.children);
const configData = computed(() => props.data.configData);
</script>
在根组件中通过provide
提供给子组件引用
js
// packages/low-code-set/src/views/pageSet/index.vue
import { ref,provide } from "vue";
provide("currComp", currComp);
provide("handleSelectComponent", handleSelectComponent);
container/index.ts
js
//container/index.ts
import Config from './config.vue'
import Render from './render.vue'
const install = (app:any) => {
app.component('ContainerConfig',Config)
app.component('Container',Render)
}
export default install
应用组件
diff
// packages/low-code-set/src/components/configs.ts
const configs = [
+ {
+ groupName: '基础组件',
+ components: [
+ {
+ render: 'Container',
+ name: '容器',
+ icon: 'House',
+ configData: {
+ style: 'position:relative;width:100%;min-height:100px;',
+ },
+ // 子组件
+ children: []
+ }
+ ]
+ },
{
groupName: '其他',
components: [
{
render: 'Image',
name: '图片',
icon: 'Picture',
configData: {
style: 'width:100%',
url: 'https://fastly.jsdelivr.net/npm/@vant/assets/cat.jpeg'
}
}
]
},
]
export default configs
diff
import Image from './image'
+import Container from './container'
const install = (app: any) => {
app.use(Image)
+ app.use(Container)
}
export default install
这样,我们就实现了Contanier组件,可以把组件拖到Contanier容器中。
HotArea
图片热点区域实现是一个难点,因为篇幅原因,会再用一章来解释图片热点的实现。
最终实现的效果如下
渲染数据如下