【架构师(第二十六篇)】编辑器开发之属性编辑同步渲染

2022-12-10 13:39:39 浏览数 (1)


属性更新

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

propsMap.ts

代码语言:javascript复制
import type { TextComponentProps } from './defaultProps';

// 属性转化成表单 哪个属性使用哪个类型的组件去编辑
export interface PropsToForm {
  component: string;
  subComponent?: string;
  extraProps?: { [key: string]: any };
  text?: string;
  options?: {
    text: string;
    value: any;
  }[];
  initalTransform?: (v: 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: (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: [
      {
        value: '',
        text: '无',
      },
      {
        value: '"SimSun","STSong',
        text: '宋体',
      },
      {
        value: '"SimHei","STHeiti',
        text: '黑体',
      },
    ],
    afterTransform: (e: any) => e,
  },
};

PropsTable.vue

代码语言:javascript复制
<template>
  <div class="props-table">
    <div v-for="(item, index) in finalProps"
         class="prop-item"
         :key="index">
      <span class="label">{{ item.text }}</span>
      <div class="prop-component">
        <!-- 使用 antd 组件库中的组件 -->
        <component v-if="item.valueProp"
                   :[item.valueProp]="item?.value"
                   v-bind="item?.extraProps"
                   v-on="item.events"
                   :is="item?.component">
          <template v-if="item.options">
            <component :is="item.subComponent"
                       v-for="(option, key) in item.options"
                       :key="key"
                       :value="option.value">
              {{ option.text }}
            </component>
          </template>
        </component>
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { defineProps, computed, defineEmits } from 'vue';
import { mapPropsToForms } from '../propsMap'
import { reduce } from 'lodash-es'
import type { PartialTextComponentProps } from '../defaultProps'
export interface Props {
  props: PartialTextComponentProps;
}
export interface FormProps {
  component: string;
  subComponent?: string;
  value: string;
  extraProps?: { [key: string]: any };
  text?: string;
  options?: {
    text: string;
    value: any;
  }[];
  initalTransform?: (v: any) => any;
  valueProp: string;
  eventName: string;
  events: { [key: string]: (e: any) => void };
}
const props = defineProps<Props>()
const emit = defineEmits<{
  (e: 'change', data: any): void;
}>()

// 获取属性表单映射列表
const finalProps = computed(() => {
  return reduce(props.props, (result, value, key) => {
    const newKey = key as keyof PartialTextComponentProps
    const item = mapPropsToForms[newKey]
    if (item) {
      const { valueProp = 'value', eventName = 'change', initalTransform, afterTransform } = item
      const newItem: FormProps = {
        ...item,
        value: initalTransform ? initalTransform(value) : value,
        valueProp,
        eventName,
        events: {
          [eventName]: (e: any) => {
            emit('change', { key, value: afterTransform ? afterTransform(e) : e })
          }
        }
      }
      result[newKey] = newItem
    }
    return result
  }, {} as { [key: string]: FormProps })
})
</script>

Editor.vue

代码语言:javascript复制
// template
  <!-- 右侧组件属性编辑 -->
  <a-layout-sider width="300"
                  style="background:#fff"
                  class="setting-container">
    组件属性
    <props-table v-if="currentElement"
                 @change="handleChange"
                 :props="currentElement?.props"></props-table>
  </a-layout-sider>
  
// script
// 更新组件的属性值事件
const handleChange = (e: any) => {
  store.commit('updateComponent', e)
}

editor.ts

代码语言:javascript复制
mutations: {
    // 更新组件属性
    updateComponent(state, { key, value }) {
      const updatedComponent = state.components.find(
        (c) => c.id === state.currentElement,
      );
      if (updatedComponent) {
        updatedComponent.props[key as keyof TextComponentProps] = value;
      }
    },
  },

最终实现如下

优化需求

选择字体的下拉框可以直接显示当前的字体样式

h 函数

h 函数接收三个参数

  • type: 元素的类型
  • props: 数据对象
  • children: 子节点

使用 h 函数改写

代码语言:javascript复制
import { h, VNode } from 'vue';
const fontFamilyArr = [
  {
    value: '"SimSun","STSong',
    text: '宋体',
  },
  {
    value: '"SimHei","STHeiti',
    text: '黑体',
  },
];
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

使用 tsx 改写

代码语言:javascript复制
const fontFamilyOptions = fontFamilyArr.map((font) => {
  return {
    value: font.value,
    text: (<span style={{ fontFamily: font.text }}>{font.text}</span>) as VNode,
  };
});

使用 render 函数实现桥梁

代码语言:javascript复制
// srccomponentsRenderVnode.ts

import { defineComponent } from 'vue';

const RenderVnode = defineComponent({
  props: {
    vNode: {
      type: [Object, String],
      required: true,
    },
  },
  render() {
    return this.vNode;
  },
});
export default RenderVnode;
代码语言:javascript复制
// srccomponentsPropsTable.vue

<!-- 使用 antd 组件库中的组件 -->
<component v-if="item.valueProp"
           :[item.valueProp]="item?.value"
           v-bind="item?.extraProps"
           v-on="item.events"
           :is="item?.component">
  <template v-if="item.options">
    <component :is="item.subComponent"
               v-for="(option, key) in item.options"
               :key="key"
               :value="option.value">
      <render-vnode :vNode="option.text"></render-vnode>
    </component>
  </template>
</component>

最终实现如下

阶段总结

业务组件

  • 创建编辑器 vuex store 结构,画布循环展示组件
  • 组件初步实现,使用 lodash 分离样式属性
  • 添加通用和特殊属性,转换为 props 类型
  • 抽取重用逻辑,style 抽取和点击跳转
  • 左侧组件库点击添加到画布的逻辑

组件属性对应表单组件的展示和更新

  • 获得正在被编辑的元素,通过 vuex getters
  • 创建属性和表单组件的对应关系
  • 使用 propsTable 将传入的属性渲染为对应的表单组件
  • 丰富对应关系字段支持更多自定义配置
  • 使用标准流程更新表单并实时同步单项数据流
  • 使用 h 函数以及 vnode 实现字体下拉框实时显示

0 人点赞