需求
- 点击某一个组件,选中组件
- 将它的属性以不同类型的表单呈现在右侧区域
-
获取正在编辑的元素的属性
组件外套一层 wrapper 用来隔离点击事件和组件自身行为
- 鼠标经过组件添加边框样式
- 点击某一个组件,选中组件,选中的组件添加高亮样式
- 点击某一个组件,向父组件 Editor.vue 发射 setActive 事件
- Editor.vue 通过 commit 更新 store 中的状态
- store 中接收组件 id,计算当前组件的属性
- Editor.vue 中接收当前组件的属性,并渲染在界面上
<template>
<div class="edit-wrapper" @click="onItemClick(id)">
<slot></slot>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
props: {
id: {
type: String,
required: true,
},
},
emits: ['on-item-click'],
setup(props, context) {
//子向父传值
const onItemClick = (id: string) => {
context.emit('on-item-click', id)
}
return { onItemClick }
},
})
</script>
<style scoped>
.edit-wrapper {
padding: 0px;
cursor: pointer;
border: 1px solid transparent;
user-select: none;
}
.edit-wrapper:hover {
border: 1px dashed #ccc;
}
.edit-wrapper.active {
border: 1px solid #1890ff;
user-select: none;
z-index: 1500;
}
</style>
...
//两个参数 第一个本地的interface,第二个是全局的interface
const editor: Module<EditorProps, GloabalDataProps> = {
state: {
components: testComponents,
currentElement: '',
},
mutations: {
...
[AETACTIVE](state, id: string) {
state.currentElement = id
},
},
getters: {
[GETCURRENTCOMPONENT](state) {
console.log(state)
return () =>
state.components.find((item) => item.id === state.currentElement)
},
},
}
export default editor
...
export const AETACTIVE = 'setActive'
export const GETCURRENTCOMPONENT = 'getCurrentComponent'
<template>
<div class="editor-container">
<a-layout>
...
<a-layout style="padding: 0 24px 24px">
<a-layout-content class="preview-container">
<p>画布区域</p>
<!--引用的是component-->
<EditWrapper
v-for="item in components"
:key="item.id"
:id="item.id"
class="l-text-wrapper"
@on-item-click="setActive"
>
<close-outlined class="icon-close" @click="delComponent(item)" />
<component
class="preview-list"
id="canvas-area"
:is="item.name"
v-bind="item.props"
>
</component>
</EditWrapper>
</a-layout-content>
</a-layout>
<a-layout-sider
width="300"
style="background: #fff"
class="settings-panel"
>
组件属性
{{ currentElement && currentElement.props }}
</a-layout-sider>
</a-layout>
</div>
</template>
<script lang="ts">
...
import EditWrapper from '@/components/EditWrapper.vue'
import {
ADDCOMPONENT,
DELCOMPONENT,
AETACTIVE,
GETCURRENTCOMPONENT,
} from '@/store/mutation-types'
export default defineComponent({
components: { LText, ComponentsList, CloseOutlined, EditWrapper },
setup() {
const store = useStore<GloabalDataProps>()
const components = computed(() => store.state.editor.components)
const currentElement = computed<ComponentData | null>(() =>
store.getters[GETCURRENTCOMPONENT](),
)
const addComponent = (data: ComponentData) => {
store.commit(ADDCOMPONENT, data)
}
const delComponent = (data: ComponentData) => {
store.commit(DELCOMPONENT, data)
}
const setActive = (id: string) => {
store.commit(AETACTIVE, id)
}
return {
components,
defaultTextTemplates,
currentElement,
addComponent,
delComponent,
setActive,
}
},
})
</script>
...
添加属性和表单的基础对应关系并展示
- 需要一个元素属性以及修改属性使用哪一种表单组件的映射表 propsMap.ts 。
- 表单部分 PropsTable.vue 接收到属性后,通过映射表获取对应关系。
- 在右侧的属性编辑区域渲染出属性对应的表单组件。
import { TextComponentProps } from '@/ts/defaultProps'
// 属性转化成表单 哪个属性使用哪个类型的组件去编辑
export interface PropsToForm {
component: string
value?: string
}
// 属性列表转化成表单列表
export type PropsToForms = {
[p in keyof TextComponentProps]?: PropsToForm
}
// 属性转化成表单的映射表 key:属性 value:使用的组件
export const mapPropsToForms: PropsToForms = {
// 比如: text 属性,使用 a-input 这个组件去编辑
text: {
component: 'a-input',
},
color: {
component: 'a-input',
},
}
<template>
<div class="props-table">
<div v-for="(item, index) in finalPros" class="prop-item" :key="index">
<component :is="item?.component" :value="item?.value"></component>
</div>
</div>
</template>
<script lang="ts">
import { TextComponentProps } from '@/ts/defaultProps'
import { mapPropsToForms, PropsToForms } from '@/ts/propsMap'
import { reduce } from 'lodash'
import { computed, defineComponent, PropType } from 'vue'
export default defineComponent({
name: 'props-table',
props: {
props: {
type: Object as PropType<TextComponentProps>,
required: true,
},
},
setup(props, context) {
const finalPros = computed(() => {
return reduce(
props.props,
(res, value, key) => {
const newKey = key as keyof TextComponentProps
const item = mapPropsToForms[newKey]
if (item) {
item.value = value
res[newKey] = item
}
return res
},
{} as PropsToForms,
)
})
console.log(finalPros)
return { finalPros }
},
})
</script>
<style>
.prop-item {
display: flex;
margin-bottom: 10px;
align-items: center;
}
.label {
width: 28%;
}
.prop-component {
width: 70%;
}
</style>
...
<a-layout-sider
width="300"
style="background: #fff"
class="settings-panel"
>
组件属性
<props-table
v-if="currentElement"
:props="currentElement?.props"
></props-table>
</a-layout-sider>
...
添加更多复杂对应关系并展示
- 每一个属性的编辑对应的是 antd 组件库的组件
- 需要给组件库的组件添加属性,如最大值,行数等
- 有的组件需要被其它组件包裹使用,需要兼容这种复杂组件
- 支持转换传入组件库属性的类型
- 支持自定义属性名称 ```typescript import { TextComponentProps } from ‘@/ts/defaultProps’
// 属性转化成表单 哪个属性使用哪个类型的组件去编辑 export interface PropsToForm { component: string value?: string // 支持给组件库传入属性 extraProps?: { [key: string]: any } text: string // 支持组件包裹 subComponent?: string // 包裹的组件选项 options?: { text: string value: any }[] // 支持类型转换 initalTransform?: (v: any) => any // 支持自定义属性名称 valueProp?: string }
// 属性列表转化成表单列表 export type PropsToForms = { [p in keyof TextComponentProps]?: PropsToForm }
// 属性转化成表单的映射表 key:属性 value:使用的组件 export const mapPropsToForms: PropsToForms = { text: { text: ‘文本’, component: ‘a-textarea’, extraProps: { rows: 3 }, }, fontSize: { text: ‘字号’, component: ‘a-input-number’, initalTransform: (v: string) => parseInt(v), }, lineHeight: { text: ‘行高’, component: ‘a-slider’, extraProps: { min: 0, max: 3, step: 0.1 }, //将行高要求float,将string转成float initalTransform: (v: string) => parseFloat(v), }, textAlign: { component: ‘a-radio-group’, subComponent: ‘a-radio-button’, text: ‘对齐’, options: [ { value: ‘left’, text: ‘左’ }, { value: ‘center’, text: ‘中’ }, { value: ‘right’, text: ‘右’ }, ], }, fontFamily: { component: ‘a-select’, subComponent: ‘a-select-option’, text: ‘字体’, options: [{ value: ‘’, text: ‘无’ }], }, color: { component: ‘color-picker’, text: ‘字体颜色’, }, }
```vue
<template>
<div class="props-table">
<div v-for="(item, index) in finalPros" class="prop-item" :key="index">
<span class="label" v-if="item.text">{{ item.text }}</span>
<div class="prop-component">
<component
v-if="item.valueProp"
:is="item?.component"
:value="item?.value"
v:bind="item.extraProps"
>
<template v-if="item.options">
<component
v-for="(option, k) in item.options"
:is="item.subComponent"
:value="option.value"
:key="k"
>
{{ option.text }} //显示label,否则不会显示内容
</component>
</template>
</component>
</div>
</div>
</div>
</template>
<script lang="ts">
import { TextComponentProps } from '@/ts/defaultProps'
import { mapPropsToForms, PropsToForms } from '@/ts/propsMap'
import { reduce } from 'lodash'
import { computed, defineComponent, PropType } from 'vue'
export default defineComponent({
name: 'props-table',
props: {
props: {
type: Object as PropType<TextComponentProps>,
required: true,
},
},
setup(props, context) {
const finalPros = computed(() => {
return reduce(
props.props,
(res, value, key) => {
// console.log(value, key)
const newKey = key as keyof TextComponentProps
const item = mapPropsToForms[newKey]
console.log(item)
if (item) {
item.value = item.initalTransform
? item.initalTransform(value)
: value
item.valueProp = item.valueProp || 'value'
res[newKey] = item
}
return res
},
//Required将可选变成必选
{} as Required<PropsToForms>,
)
})
return { finalPros }
},
})
</script>
...