需求
- 点击某一个组件,选中组件
- 将它的属性以不同类型的表单呈现在右侧区域
-
获取正在编辑的元素的属性
组件外套一层 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,第二个是全局的interfaceconst 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--><EditWrapperv-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)" /><componentclass="preview-list"id="canvas-area":is="item.name"v-bind="item.props"></component></EditWrapper></a-layout-content></a-layout><a-layout-siderwidth="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: stringvalue?: 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 TextComponentPropsconst item = mapPropsToForms[newKey]if (item) {item.value = valueres[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-siderwidth="300"style="background: #fff"class="settings-panel">组件属性<props-tablev-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"><componentv-if="item.valueProp":is="item?.component":value="item?.value"v:bind="item.extraProps"><template v-if="item.options"><componentv-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 TextComponentPropsconst item = mapPropsToForms[newKey]console.log(item)if (item) {item.value = item.initalTransform? item.initalTransform(value): valueitem.valueProp = item.valueProp || 'value'res[newKey] = item}return res},//Required将可选变成必选{} as Required<PropsToForms>,)})return { finalPros }},})</script>...

