属性更新
- 属性编辑通过 store 获取属性值
- 通过发射事件触发 commit 修改属性值
- 支持属性值的转换

import { TextComponentProps } from '@/ts/defaultProps'// 属性转化成表单 哪个属性使用哪个类型的组件去编辑export interface PropsToForm {component: string// 支持给组件库传入属性extraProps?: { [key: string]: any }text: string// 支持组件包裹subComponent?: string// 包裹的组件选项options?: {text: stringvalue: any}[]// 支持类型转换initalTransform?: (v: any) => any//因为开始定义的是any,这样会各个组件的类型不同发生错误,处理更新后的函数afterTransform?: (v: any) => any// 支持自定义属性名称valueProp?: string//触发事件的名称eventName?: string}// 属性列表转化成表单列表export type PropsToForms = {[p in keyof TextComponentProps]?: PropsToForm}// 属性转化成表单的映射表 key:属性 value:使用的组件export const mapPropsToForms: PropsToForms = {// 比如: text 属性,使用 a-input 这个组件去编辑text: {component: 'a-textarea',extraProps: {rows: 3,},text: '文本',afterTransform: (e: any) => e.target.value,},fontSize: {text: '字号',component: 'a-input-number',initalTransform: (v: string) => parseInt(v),afterTransform: (e: any) => (e ? `${e}px` : ''),},lineHeight: {text: '行高',component: 'a-slider',extraProps: {min: 0,max: 3,step: 0.1,},initalTransform: (v: string) => parseFloat(v),afterTransform: (v: string) => String(v),},textAlign: {component: 'a-radio-group',subComponent: 'a-radio-button',text: '对齐',options: [{value: 'left',text: '左',},{value: 'center',text: '中',},{value: 'right',text: '右',},],afterTransform: (e: any) => e.target.value,},fontFamily: {component: 'a-select',subComponent: 'a-select-option',text: '字体',options: [{value: '',text: '无',},{value: '"SimSun","STSong',text: '宋体',},{value: '"SimHei","STHeiti',text: '黑体',},],afterTransform: (e: any) => e,},}
<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"><!--v-bind 绑定属性 v-on 绑定事件 重点--><componentv-if="item.valueProp":is="item?.component":value="item?.valueProp"v-bind="item.extraProps"v-on="item.events"><template v-if="item.options"><componentv-for="(option, k) in item.options":is="item.subComponent":value="option.value":key="k">{{ option.text }}</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 interface FormProps {component: stringsubComponent?: stringvalue: stringextraProps?: { [key: string]: any }text?: stringoptions?: {text: stringvalue: any}[]initalTransform?: (v: any) => anyvalueProp: stringeventName: stringevents: { [key: string]: (e: any) => void }}export default defineComponent({name: 'props-table',props: {props: {type: Object as PropType<TextComponentProps>,required: true,},},emits: ['change'],setup(props, context) {const finalPros = computed(() => {//重点1return reduce(props.props,(res, value, key) => {// console.log(value, key) //value是每一个组件属性值 value表示属性const newKey = key as keyof TextComponentPropsconst item = mapPropsToForms[newKey]if (item) {const {valueProp = value,eventName = 'change',initalTransform,afterTransform,} = item//转成特定的form表单const newItem: FormProps = {...item,value: initalTransform ? initalTransform(value) : value,valueProp: initalTransform? initalTransform(valueProp): valueProp,eventName,events: {[eventName]: (e: any) => {context.emit('change', {key,value: afterTransform ? afterTransform(e) : e,})},},}res[newKey] = newItem}// console.log(res)return res},{} as { [key: string]: FormProps },)})return { finalPros }},})</script>...
<template><div class="editor-container">...<a-layout-siderwidth="300"style="background: #fff"class="settings-panel">组件属性<props-tablev-if="currentElement && currentElement?.props":props="currentElement?.props"@change="handleChange"></props-table></a-layout-sider></a-layout></div></template><script lang="ts">...import {ADDCOMPONENT,DELCOMPONENT,AETACTIVE,GETCURRENTCOMPONENT,UPDATECOMPONENT,} from '@/store/mutation-types'export default defineComponent({components: { LText, ComponentsList, CloseOutlined, EditWrapper, PropsTable },setup() {...const handleChange = (e: any) => {console.log('event', e)store.commit(UPDATECOMPONENT, e)}return {handleChange,}},})</script>...
import {ADDCOMPONENT,DELCOMPONENT,AETACTIVE,GETCURRENTCOMPONENT,UPDATECOMPONENT,} from './mutation-types'...//两个参数 第一个本地的interface,第二个是全局的interfaceconst editor: Module<EditorProps, GloabalDataProps> = {state: {components: testComponents,currentElement: '',},mutations: {...[UPDATECOMPONENT](state, { key, value }) {const updatedComponent = state.components.find((component) => component.id === state.currentElement,)if (updatedComponent) {updatedComponent.props[key as keyof TextComponentProps] = value}},},...}export default editor
优化需求
使用 h 函数改写
...import { h, VNode } from 'vue';const fontFamilyArr = [{ text: '宋体', value: '"SimSun","STSong"' },{ text: '黑体', value: '"SimHei","STHeiti"' },{ text: '楷体', value: '"KaiTi","STKaiti"' },{ text: '仿宋', value: '"FangSong","STFangsong"' },]const fontFamilyOptions = fontFamilyArr.map((font) => {return {value: font.value,text: h('span', { style: { fontFamily: font.value } }, font.text),};});// 属性转化成表单的映射表 key:属性 value:使用的组件export const mapPropsToForms: PropsToForms = {fontFamily: {component: 'a-select',subComponent: 'a-select-option',text: '字体',options: [{value: '',text: '无',},...fontFamilyOptions,],afterTransform: (e: any) => e,},};...
tsx
import { TextComponentProps } from '@/ts/defaultProps'import { VNode } from 'vue'// 属性转化成表单 哪个属性使用哪个类型的组件去编辑export interface PropsToForm {component: string// 支持给组件库传入属性extraProps?: { [key: string]: any }text?: string// 支持组件包裹subComponent?: string// 包裹的组件选项options?: {text: string | VNodevalue: any}[]// 支持类型转换initalTransform?: (v: any) => anyafterTransform?: (v: any) => any// 支持自定义属性名称valueProp?: string//触发事件的名称eventName?: string}// 属性列表转化成表单列表export type PropsToForms = {[p in keyof TextComponentProps]?: PropsToForm}const fontFamilyArr = [{ text: '宋体', value: '"SimSun","STSong"' },{ text: '黑体', value: '"SimHei","STHeiti"' },{ text: '楷体', value: '"KaiTi","STKaiti"' },{ text: '仿宋', value: '"FangSong","STFangsong"' },]const fontFamilyOptions = fontFamilyArr.map((font) => {return {value: font.value,text: (<span style={{ fontFamily: font.value }}>{font.text}</span>) as VNode,}})// 属性转化成表单的映射表 key:属性 value:使用的组件const pxToNumberHandler: PropsToForm = {component: 'a-input-number',initalTransform: (v: string) => parseInt(v),afterTransform: (e: number) => (e ? `${e}px` : ''),}export const mapPropsToForms: PropsToForms = {text: {text: '文本',component: 'a-textarea',extraProps: { rows: 3 },afterTransform: (e: any) => e.target.value,},fontSize: {text: '字号',...pxToNumberHandler,},lineHeight: {text: '行高',component: 'a-slider',extraProps: { min: 0, max: 3, step: 0.1 },initalTransform: (v: string) => parseFloat(v),afterTransform: (e: number) => e.toString(),},textAlign: {component: 'a-radio-group',subComponent: 'a-radio-button',text: '对齐',options: [{ value: 'left', text: '左' },{ value: 'center', text: '中' },{ value: 'right', text: '右' },],afterTransform: (e: any) => e.target.value,},fontFamily: {component: 'a-select',subComponent: 'a-select-option',text: '字体',options: fontFamilyOptions,},width: {text: '宽度',...pxToNumberHandler,},}
方案一:使用 JSX 重写 PropsTable 组件(较推荐)
import { computed, defineComponent, PropType, VNode } from 'vue'import { reduce } from 'lodash'import { PropsToForms, mapPropsToForms } from '../propsMap'import { TextComponentProps } from '../defaultProps'import { Input, InputNumber, Slider, Radio, Select } from 'ant-design-vue'const mapToComponent = {'a-textarea': Input.TextArea,'a-input-number': InputNumber,'a-slider': Slider,'a-radio-group': Radio.Group,'a-radio-button': Radio.Button,'a-select': Select,'a-select-option': Select.Option} as anyinterface FormProps {component: string;subComponent?: string;value: string;extraProps?: { [key: string]: any };text?: string;options?: { text: string | VNode; value: any }[];valueProp: string;eventName: string;events: { [key: string]: (e: any) => void };}function capitalizeFirstLetter(string: string) {return string.charAt(0).toUpperCase() + string.slice(1)}export default defineComponent({name: 'props-table',props: {props: {type: Object as PropType<TextComponentProps>,required: true}},emits: ['change'],setup(props, context) {const finalProps = computed(() => {return reduce(props.props, (result, value, key) => {const newKey = key as keyof TextComponentPropsconst item = mapPropsToForms[newKey]if (item) {const { valueProp = 'value', eventName = 'change', initalTransform, afterTransform } = itemconst newItem: FormProps = {...item,value: initalTransform ? initalTransform(value) : value,valueProp,eventName,events: {['on' + capitalizeFirstLetter(eventName)]: (e: any) => { context.emit('change', { key, value: afterTransform? afterTransform(e) : e })}}}result[newKey] = newItem}return result}, {} as { [key: string]: FormProps })})return () =><div class="props-table">{Object.keys(finalProps.value).map(key => {const value = finalProps.value[key]const ComponentName = mapToComponent[value.component]const SubComponent = value.subComponent ? mapToComponent[value.subComponent] : nullconst props = {[value.valueProp]: value.value,...value.extraProps,...value.events}return (<div key={key} class="prop-item">{ value.text && <span class="label">{value.text}</span> }<div class="prop-component"><ComponentName {...props}>{ value.options &&value.options.map(option => {return (<SubComponent value={option.value}>{option.text}</SubComponent>)})}</ComponentName></div></div>)})}</div>}})
方案二:使用 render 函数实现桥梁
增加返回render的Vnode
import { defineComponent } from 'vue'const renderNode = defineComponent({name: 'render-node',props: {vNode: {type: [Object, String],required: true,},},render() {return this.vNode},})export default renderNode
修改{{option.text}}成render-node组件
<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"><!--v-bind 绑定属性 v-on 绑定事件--><componentv-if="item.valueProp":is="item?.component":value="item?.valueProp"v-bind="item.extraProps"v-on="item.events"><template v-if="item.options"><componentv-for="(option, k) in item.options":is="item.subComponent":value="option.value":key="k"><render-node :vNode="option.text"></render-node></component></template></component></div></div></div></template><script lang="ts">...export default defineComponent({name: 'props-table',...components: { RenderNode },...})</script>
处理编辑时跳转问题
import { computed } from 'vue'import _ from 'lodash-es'export default function useComponentCommon<T extends { [key: string]: any }>(props: T,picks: string[],) {const styleProps = computed(() => _.pick(props, picks))const handleClick = () => {if (window.location.hash !== '#/editor' &&props.actionType === 'url' &&props.url) {window.location.href = props.url}}//事件类型:无|跳转URL下拉菜单return { styleProps, handleClick }}
修改data数据,字体为空时不显示数据
...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 default editor
总结:
业务组件
- 创建编辑器 vuex store 结构,画布循环展示组件
- 组件初步实现,使用 lodash 分离样式属性
- 添加通用和特殊属性,转换为 props 类型
- 抽取重用逻辑,style 抽取和点击跳转
- 左侧组件库点击添加到画布的逻辑
组件属性对应表单组件的展示和更新
- 获得正在被编辑的元素,通过 vuex getters
- 创建属性和表单组件的对应关系
- 使用 propsTable 将传入的属性渲染为对应的表单组件
- 丰富对应关系字段支持更多自定义配置
- 使用标准流程更新表单并实时同步单项数据流
- 使用 h 函数以及 vnode 实现字体下拉框实时显示
存在问题:设置为0或者’’时,组件会消失,后续记得改
