Continue to create, accelerate growth! This is the third day of my participation in the "Nuggets Daily New Plan · June Update Challenge", click to view the details of the event
data structure
component data structure
- props : Properties of the component, including style properties and some other properties, such as
url
,action
etc. - id : The
id
unique identifier of the component,uuid
generated . - name : the name of the component, the
:is
property
Editor data structure
- components : List of components, which components are added to the current canvas.
- currentElement : The active component, representing the component currently being edited.
Other knowledge points
- Module
vuex
: Provide a type for the module, the first parameter is the type of the current module, and the second parameter isstore
the type of the entire .
import type { Module } from 'vuex';
import type { GlobalStore } from './index';
import { v4 as uuidv4 } from 'uuid';
// 组件数据结构
export interface ComponentData {
// 这个元素的属性
props: { [key: string]: unknown };
// id, uuid v4 生成
id: string;
// 业务组件库的名称 l-text , l-image 等,动态组件渲染的组件名称。
name: string;
}
// 编辑器数据结构
export interface EditorStore {
// 供中间编辑器渲染的数据
components: ComponentData[];
// 当前编辑的是哪一个元素 , uuid
currentElement: string;
}
// 测试数据
const testComponents: ComponentData[] = [
{
id: uuidv4(),
name: 'l-text',
props: {
text: 'hello',
fontSize: '20px',
tag: 'div',
},
},
{
id: uuidv4(),
name: 'l-text',
props: {
text: 'hello2',
fontSize: '14px',
tag: 'div',
color: 'red',
},
},
{
id: uuidv4(),
name: 'l-text',
props: {
text: 'hello3',
tag: 'div',
fontSize: '12px',
fontWeight: '800',
actionType: 'url',
url: 'http://www.baidu.com',
},
},
];
const editorStore: Module<EditorStore, GlobalStore> = {
state: {
// 组件列表
components: testComponents,
// 当前操作的组件
currentElement: '',
},
};
export default editorStore;
复制代码
Basic layout
Use ant-design-vue
for basic layout, including header
, component list area, canvas component editing area, component property editing area.
<template>
<div class="editor"
id="editor-layout-main">
<!-- header -->
<a-layout :style="{ background: '#fff' }">
<a-layout-header class="header">
<div class="page-title"
:style="{ color: '#fff' }">
慕课乐高
</div>
</a-layout-header>
</a-layout>
<a-layout>
<!-- 左侧组件列表 -->
<a-layout-sider width="300"
style="background:yellow">
<div class="sidebar-container">
组件列表
</div>
</a-layout-sider>
<!-- 中间画布编辑区域 -->
<a-layout style="padding:0 24px 24px">
<a-layout-content class="preview-container">
<p>画布区域</p>
<!-- 组件列表 -->
<div class="preview-list"
id="canvas-area">
<!-- 使用动态组件进行渲染 -->
<component v-for="component in components"
:key="component.id"
:is="component.name"
v-bind="component.props"></component>
</div>
</a-layout-content>
</a-layout>
<!-- 右侧组件属性编辑 -->
<a-layout-sider width="300"
style="background:purple"
class="setting-container">
组件属性
</a-layout-sider>
</a-layout>
</div>
</template>
<script lang="ts">
import { computed, defineComponent } from 'vue';
import { useStore } from 'vuex';
import { GlobalStore } from '../store/index'
import LText from '../components/LText.vue'
export default defineComponent({
components: {
LText
},
setup() {
// 从 store 里获取数据,使用泛型以获得类型
const store = useStore<GlobalStore>()
// 从 store 里回组件列表
const components = computed(() => store.state.editor.components)
return {
components
}
}
})
</script>
<style scoped>
.editor {
width: 100%;
height: 100%;
}
.ant-layout-has-sider {
height: calc(100% - 64px);
}
.preview-list {
background: #fff;
position: relative;
}
</style>
复制代码
Using option Api
both and Composition API
, I think this is not a good way, so I plan to use setup
Syntactic sugar to rewrite, but when using dynamic components :is
, if you do not register components explicitly, the final rendered result is a custom tag, rather than the components we wrote.
// template
<component v-for="component in components"
:key="component.id"
:is="component.name"
v-bind="component.props"></component>
// 使用 setup 语法糖时
<script lang="ts" setup>
import { computed } from 'vue';
import { useStore } from 'vuex';
import { GlobalStore } from '../store/index'
// 只是引用而没有显式的注册
import LText from '../components/LText.vue'
// 从 store 里获取数据,使用泛型以获得类型
const store = useStore<GlobalStore>()
// 从 store 里回组件列表
const components = computed(() => store.state.editor.components)
</script>
复制代码
dom
The structure will render like this, which is obviously not what we want.
In this regard, the official website gives an explanation, let's take a look.
So when we use setup
syntactic sugar, there is no way to explicitly register components ( maybe I don't know the method ), so we can only use the second way to bind an imported component object, which requires more Write a mapping table of component objects and component names, and this will solve the problem.
// template
<component v-for="component in components"
:key="component.id"
:is="componentMap[component.name]"
v-bind="component.props"></component>
// 使用 setup 语法糖时
<script lang="ts" setup>
// 组件实例映射表的类型
export interface ComponentMap {
[key: string]: Component;
}
import { computed, Component } from 'vue';
import { useStore } from 'vuex';
import { GlobalStore } from '../store/index'
import LText from '../components/LText.vue'
// 从 store 里获取数据,使用泛型以获得类型
const store = useStore<GlobalStore>()
// 从 store 里回组件列表
const components = computed(() => store.state.editor.components)
// 组件实例映射表
const componentMap: ComponentMap = {
'l-text': LText
}
</script>
复制代码
L-Text Components
Configure common default properties for components as well as l-text
component -specific default properties.
import { mapValues, without } from 'lodash-es';
// 通用的默认属性
export const commonDefaultProps = {
// actions
actionType: '',
url: '',
// size
height: '',
width: '318px',
paddingLeft: '0px',
paddingRight: '0px',
paddingTop: '0px',
paddingBottom: '0px',
// border type
borderStyle: 'none',
borderColor: '#000',
borderWidth: '0',
borderRadius: '0',
// shadow and opacity
boxShadow: '0 0 0 #000000',
opacity: '1',
// position and x,y
position: 'absolute',
top: '0',
left: '0',
right: '0',
bottom: '0',
};
// l-text 组件特有默认属性
export const textDefaultProps = {
// basic props - font styles
text: '正文内容',
fontSize: '14px',
fontFamily: '',
fontWeight: 'normal',
fontStyle: 'normal',
textDecoration: 'none',
lineHeight: '1',
textAlign: 'left',
color: '#000000',
backgroundColor: '',
...commonDefaultProps,
};
// 排除非样式属性
export const textStylePropNames = without(
Object.keys(textDefaultProps),
'actionType',
'url',
'text',
);
// 转换成组件的props属性
export const transformToComponentProps = <T extends { [key: string]: any }>(
props: T,
) => {
return mapValues(props, (item) => {
return {
type: item.constructor,
default: item,
};
});
};
复制代码
Wraps one hooks
, picks out style properties, and returns a click event handler.
import { computed } from 'vue';
import { pick } from 'lodash-es';
// 使用 lodash 的 pick 方法挑选出样式属性,并返回一个点击事件处理函数
const useComponentCommon = <T extends { [key: string]: any }>(
props: T,
picks: string[],
) => {
const styleProps = computed(() => pick(props, picks));
const handleClick = () => {
if (props.actionType === 'url' && props.url) {
window.location.href = props.url;
}
};
return {
styleProps,
handleClick,
};
};
export default useComponentCommon;
复制代码
- I was lazy here and didn't use
setup
syntactic sugar to rewrite it. :is
The boundtag
property is the rendered label type.
<template>
<!-- 使用动态组件进行渲染 -->
<component :is="tag"
:style="styleProps"
@click="handleClick"
class="l-text-component">
{{ text }}
</component>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { transformToComponentProps, textDefaultProps, textStylePropNames } from '../defaultProps'
import useComponentCommon from '../hooks/useComponentCommon'
const defaultProps = transformToComponentProps(textDefaultProps)
export default defineComponent({
// 合并 props
props: {
tag: {
type: String,
default: 'div'
},
...defaultProps
},
setup(props) {
// 获取到样式属性
const { styleProps, handleClick } = useComponentCommon(props, textStylePropNames)
return {
styleProps,
handleClick
}
}
})
</script>
<style scoped>
h2.l-text-component,
p.l-text-component {
margin-bottom: 0;
}
button.l-text-component {
padding: 5px 10px;
cursor: pointer;
}
.l-text-component {
box-sizing: border-box;
white-space: pre-wrap;
position: relative !important;
}
</style>
复制代码
The final result is as follows, the l-text
component is rendered to the canvas area, which is a bit ugly, but the focus is not on the style, it is not important.