流程图
点击模版列表添加到画布的行为流程图如下。
- TemplatesData 为本地写死的模板数据,内部包含 l-text 组件的各个属性 。
- 通过 Editor.vue 把 TemplatesData 传递给 ComponentsList.vue 组件。
- ComponentsList.vue 组件接受并渲染父组件传递过来的 TemplatesData。
- 在 l-text 外层包裹 Wrapper 用来传递事件。
- 当点击 Wrapper 的时候,发送事件 onItemClick。
- 父组件执行 store.commit 向 store 中添加一条数据。
- 由于 store 是响应式的,所以当 store 多了一条数据的时候,画布就会自动渲染出来。
vscode-code-to-type ,复制 js 对象,生成 ts 类型
添加/删除模版到画布
export const defaultTextTemplates = [
{
text: '大标题',
fontSize: '30px',
fontWeight: 'bold',
tag: 'h2',
},
{
text: '正文内容',
tag: 'p',
},
{
text: '链接内容',
color: '#1890ff',
textDecoration: 'underline',
tag: 'p',
},
{
text: '按钮内容',
color: '#ffffff',
backgroundColor: '#1890ff',
borderWidth: '1px',
borderColor: '#1890ff',
borderStyle: 'solid',
borderRadius: '2px',
paddingLeft: '10px',
paddingRight: '10px',
paddingTop: '5px',
paddingBottom: '5px',
width: '100px',
tag: 'button',
textAlign: 'center',
},
]
import { Module } from 'vuex'
import { v4 as uuidv4 } from 'uuid'
import { ImageComponentProps, TextComponentProps } from '@/ts/defaultProps'
import { GloabalDataProps } from '.'
import { ADDCOMPONENT, DELCOMPONENT } from './mutation-types'
export interface ComponentData {
// 这个元素的 属性,属性请详见下面
props: Partial<TextComponentProps & ImageComponentProps>
// id,uuid v4 生成
id: string
// 业务组件库名称 l-text,l-image 等等
name: 'l-text' | 'l-image'
}
export const testComponents: ComponentData[] = [
{
id: uuidv4(),
name: 'l-text',
props: {
text: 'hello',
fontSize: '20px',
color: '#000000',
lineHeight: '1',
textAlign: 'left',
fontFamily: '',
},
},
{
id: uuidv4(),
name: 'l-text',
props: {
text: 'hello2',
fontSize: '10px',
fontWeight: 'bold',
lineHeight: '2',
textAlign: 'left',
fontFamily: '',
},
},
{
id: uuidv4(),
name: 'l-text',
props: {
text: 'hello3',
fontSize: '15px',
actionType: 'url',
url: 'https://www.baidu.com',
lineHeight: '3',
textAlign: 'left',
fontFamily: '',
},
},
]
export interface EditorProps {
// 供中间编辑器渲染的数组
components: ComponentData[]
// 当前编辑的是哪个元素,uuid
currentElement: string
}
//两个参数 第一个本地的interface,第二个是全局的interface
const editor: Module<EditorProps, GloabalDataProps> = {
state: {
components: testComponents,
currentElement: '',
},
mutations: {
[ADDCOMPONENT](state, componentData: ComponentData) {
state.components.push(componentData)
},
[DELCOMPONENT](state, componentData: ComponentData) {
const res = state.components.filter(
(item) => item.id !== componentData.id,
)
state.components = res
},
},
getters: {},
}
export default editor
<template>
<div class="create-component-list">
<div
v-for="(item, index) in list"
:key="index"
class="component-item"
@click="onItemClick(index)"
>
<l-text v-bind="item"></l-text>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import { v4 as uuidv4 } from 'uuid'
import { message } from 'ant-design-vue'
import LText from '@/components/LText.vue'
import props from 'ant-design-vue/lib/dropdown/props'
import { ComponentData } from '@/store/editor'
export default defineComponent({
props: {
list: {
type: Array,
default: [],
},
},
components: { LText },
emits: ['on-item-click'],
setup(props, context) {
//子向父传值
const onItemClick = (index: number) => {
const componentData = {
name: 'l-text',
id: uuidv4(),
props: props.list[index],
}
context.emit('on-item-click', componentData)
}
return { onItemClick }
},
})
</script>
<style>
.component-item {
width: 100px;
margin: 0 auto;
margin-bottom: 15px;
}
</style>
<template>
<div class="editor-container">
<a-layout>
<a-layout-sider width="300" style="background: #fff">
<div class="sidebar-container">组件列表</div>
<components-list
:list="defaultTextTemplates"
@on-item-click="addComponent"
></components-list>
</a-layout-sider>
<a-layout style="padding: 0 24px 24px">
<a-layout-content class="preview-container">
<p>画布区域</p>
<!--引用的是component-->
<div v-for="item in components" :key="item.id" class="l-text-wrapper">
<close-outlined class="icon-close" @click="delComponent(item)" />
<component
class="preview-list"
id="canvas-area"
:is="item.name"
v-bind="item.props"
>
</component>
</div>
</a-layout-content>
</a-layout>
<a-layout-sider
width="300"
style="background: #fff"
class="settings-panel"
>
组件属性
</a-layout-sider>
</a-layout>
</div>
</template>
<script lang="ts">
import { GloabalDataProps } from '@/store'
import { defineComponent, computed } from 'vue'
import { useStore } from 'vuex'
import LText from '@/components/LText.vue'
import { CloseOutlined } from '@ant-design/icons-vue'
import ComponentsList from '@/components/ComponentsList.vue'
import { defaultTextTemplates } from '@/ts/defaultTemplates'
import { ComponentData } from '@/store/editor'
import { ADDCOMPONENT, DELCOMPONENT } from '@/store/mutation-types'
export default defineComponent({
components: { LText, ComponentsList, CloseOutlined },
setup() {
const store = useStore<GloabalDataProps>()
const components = computed(() => store.state.editor.components)
const addComponent = (data: ComponentData) => {
store.commit(ADDCOMPONENT, data)
}
const delComponent = (data: ComponentData) => {
console.log(data)
store.commit(DELCOMPONENT, data)
}
return {
components,
defaultTextTemplates,
addComponent,
delComponent,
}
},
})
</script>
<style lang="less" scoped>
.icon-close {
position: absolute;
z-index: 999;
right: 0;
}
.l-text-wrapper {
position: relative;
border: 1px solid #999;
margin: 2px 0;
}
.editor-container .preview-container {
margin: 0;
display: flex;
flex-direction: column;
// align-items: center;
position: relative;
}
.editor-container .preview-list {
padding: 0;
margin: 0;
border: 1px solid #efefef;
background: #fff;
overflow-x: hidden;
overflow-y: auto;
position: fixed;
}
</style>