需求

  • 点击某一个组件,选中组件
  • 将它的属性以不同类型的表单呈现在右侧区域
  • 编辑表单中的值,在值更新的同时,将数据更新到界面

    获取正在编辑的元素的属性

  • 组件外套一层 wrapper 用来隔离点击事件和组件自身行为

  • 鼠标经过组件添加边框样式
  • 点击某一个组件,选中组件,选中的组件添加高亮样式
  • 点击某一个组件,向父组件 Editor.vue 发射 setActive 事件
  • Editor.vue 通过 commit 更新 store 中的状态
  • store 中接收组件 id,计算当前组件的属性
  • Editor.vue 中接收当前组件的属性,并渲染在界面上

3编辑器开发之属性编辑区域表单渲染 - 图1

  1. <template>
  2. <div class="edit-wrapper" @click="onItemClick(id)">
  3. <slot></slot>
  4. </div>
  5. </template>
  6. <script lang="ts">
  7. import { defineComponent } from 'vue'
  8. export default defineComponent({
  9. props: {
  10. id: {
  11. type: String,
  12. required: true,
  13. },
  14. },
  15. emits: ['on-item-click'],
  16. setup(props, context) {
  17. //子向父传值
  18. const onItemClick = (id: string) => {
  19. context.emit('on-item-click', id)
  20. }
  21. return { onItemClick }
  22. },
  23. })
  24. </script>
  25. <style scoped>
  26. .edit-wrapper {
  27. padding: 0px;
  28. cursor: pointer;
  29. border: 1px solid transparent;
  30. user-select: none;
  31. }
  32. .edit-wrapper:hover {
  33. border: 1px dashed #ccc;
  34. }
  35. .edit-wrapper.active {
  36. border: 1px solid #1890ff;
  37. user-select: none;
  38. z-index: 1500;
  39. }
  40. </style>
  1. ...
  2. //两个参数 第一个本地的interface,第二个是全局的interface
  3. const editor: Module<EditorProps, GloabalDataProps> = {
  4. state: {
  5. components: testComponents,
  6. currentElement: '',
  7. },
  8. mutations: {
  9. ...
  10. [AETACTIVE](state, id: string) {
  11. state.currentElement = id
  12. },
  13. },
  14. getters: {
  15. [GETCURRENTCOMPONENT](state) {
  16. console.log(state)
  17. return () =>
  18. state.components.find((item) => item.id === state.currentElement)
  19. },
  20. },
  21. }
  22. export default editor
  1. ...
  2. export const AETACTIVE = 'setActive'
  3. export const GETCURRENTCOMPONENT = 'getCurrentComponent'
  1. <template>
  2. <div class="editor-container">
  3. <a-layout>
  4. ...
  5. <a-layout style="padding: 0 24px 24px">
  6. <a-layout-content class="preview-container">
  7. <p>画布区域</p>
  8. <!--引用的是component-->
  9. <EditWrapper
  10. v-for="item in components"
  11. :key="item.id"
  12. :id="item.id"
  13. class="l-text-wrapper"
  14. @on-item-click="setActive"
  15. >
  16. <close-outlined class="icon-close" @click="delComponent(item)" />
  17. <component
  18. class="preview-list"
  19. id="canvas-area"
  20. :is="item.name"
  21. v-bind="item.props"
  22. >
  23. </component>
  24. </EditWrapper>
  25. </a-layout-content>
  26. </a-layout>
  27. <a-layout-sider
  28. width="300"
  29. style="background: #fff"
  30. class="settings-panel"
  31. >
  32. 组件属性
  33. {{ currentElement && currentElement.props }}
  34. </a-layout-sider>
  35. </a-layout>
  36. </div>
  37. </template>
  38. <script lang="ts">
  39. ...
  40. import EditWrapper from '@/components/EditWrapper.vue'
  41. import {
  42. ADDCOMPONENT,
  43. DELCOMPONENT,
  44. AETACTIVE,
  45. GETCURRENTCOMPONENT,
  46. } from '@/store/mutation-types'
  47. export default defineComponent({
  48. components: { LText, ComponentsList, CloseOutlined, EditWrapper },
  49. setup() {
  50. const store = useStore<GloabalDataProps>()
  51. const components = computed(() => store.state.editor.components)
  52. const currentElement = computed<ComponentData | null>(() =>
  53. store.getters[GETCURRENTCOMPONENT](),
  54. )
  55. const addComponent = (data: ComponentData) => {
  56. store.commit(ADDCOMPONENT, data)
  57. }
  58. const delComponent = (data: ComponentData) => {
  59. store.commit(DELCOMPONENT, data)
  60. }
  61. const setActive = (id: string) => {
  62. store.commit(AETACTIVE, id)
  63. }
  64. return {
  65. components,
  66. defaultTextTemplates,
  67. currentElement,
  68. addComponent,
  69. delComponent,
  70. setActive,
  71. }
  72. },
  73. })
  74. </script>
  75. ...

image.png

添加属性和表单的基础对应关系并展示

  • 需要一个元素属性以及修改属性使用哪一种表单组件的映射表 propsMap.ts 。
  • 表单部分 PropsTable.vue 接收到属性后,通过映射表获取对应关系。
  • 在右侧的属性编辑区域渲染出属性对应的表单组件。

3编辑器开发之属性编辑区域表单渲染 - 图3

  1. import { TextComponentProps } from '@/ts/defaultProps'
  2. // 属性转化成表单 哪个属性使用哪个类型的组件去编辑
  3. export interface PropsToForm {
  4. component: string
  5. value?: string
  6. }
  7. // 属性列表转化成表单列表
  8. export type PropsToForms = {
  9. [p in keyof TextComponentProps]?: PropsToForm
  10. }
  11. // 属性转化成表单的映射表 key:属性 value:使用的组件
  12. export const mapPropsToForms: PropsToForms = {
  13. // 比如: text 属性,使用 a-input 这个组件去编辑
  14. text: {
  15. component: 'a-input',
  16. },
  17. color: {
  18. component: 'a-input',
  19. },
  20. }
  1. <template>
  2. <div class="props-table">
  3. <div v-for="(item, index) in finalPros" class="prop-item" :key="index">
  4. <component :is="item?.component" :value="item?.value"></component>
  5. </div>
  6. </div>
  7. </template>
  8. <script lang="ts">
  9. import { TextComponentProps } from '@/ts/defaultProps'
  10. import { mapPropsToForms, PropsToForms } from '@/ts/propsMap'
  11. import { reduce } from 'lodash'
  12. import { computed, defineComponent, PropType } from 'vue'
  13. export default defineComponent({
  14. name: 'props-table',
  15. props: {
  16. props: {
  17. type: Object as PropType<TextComponentProps>,
  18. required: true,
  19. },
  20. },
  21. setup(props, context) {
  22. const finalPros = computed(() => {
  23. return reduce(
  24. props.props,
  25. (res, value, key) => {
  26. const newKey = key as keyof TextComponentProps
  27. const item = mapPropsToForms[newKey]
  28. if (item) {
  29. item.value = value
  30. res[newKey] = item
  31. }
  32. return res
  33. },
  34. {} as PropsToForms,
  35. )
  36. })
  37. console.log(finalPros)
  38. return { finalPros }
  39. },
  40. })
  41. </script>
  42. <style>
  43. .prop-item {
  44. display: flex;
  45. margin-bottom: 10px;
  46. align-items: center;
  47. }
  48. .label {
  49. width: 28%;
  50. }
  51. .prop-component {
  52. width: 70%;
  53. }
  54. </style>
  1. ...
  2. <a-layout-sider
  3. width="300"
  4. style="background: #fff"
  5. class="settings-panel"
  6. >
  7. 组件属性
  8. <props-table
  9. v-if="currentElement"
  10. :props="currentElement?.props"
  11. ></props-table>
  12. </a-layout-sider>
  13. ...

image.png

添加更多复杂对应关系并展示

  • 每一个属性的编辑对应的是 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: ‘字体颜色’, }, }

  1. ```vue
  2. <template>
  3. <div class="props-table">
  4. <div v-for="(item, index) in finalPros" class="prop-item" :key="index">
  5. <span class="label" v-if="item.text">{{ item.text }}</span>
  6. <div class="prop-component">
  7. <component
  8. v-if="item.valueProp"
  9. :is="item?.component"
  10. :value="item?.value"
  11. v:bind="item.extraProps"
  12. >
  13. <template v-if="item.options">
  14. <component
  15. v-for="(option, k) in item.options"
  16. :is="item.subComponent"
  17. :value="option.value"
  18. :key="k"
  19. >
  20. {{ option.text }} //显示label,否则不会显示内容
  21. </component>
  22. </template>
  23. </component>
  24. </div>
  25. </div>
  26. </div>
  27. </template>
  28. <script lang="ts">
  29. import { TextComponentProps } from '@/ts/defaultProps'
  30. import { mapPropsToForms, PropsToForms } from '@/ts/propsMap'
  31. import { reduce } from 'lodash'
  32. import { computed, defineComponent, PropType } from 'vue'
  33. export default defineComponent({
  34. name: 'props-table',
  35. props: {
  36. props: {
  37. type: Object as PropType<TextComponentProps>,
  38. required: true,
  39. },
  40. },
  41. setup(props, context) {
  42. const finalPros = computed(() => {
  43. return reduce(
  44. props.props,
  45. (res, value, key) => {
  46. // console.log(value, key)
  47. const newKey = key as keyof TextComponentProps
  48. const item = mapPropsToForms[newKey]
  49. console.log(item)
  50. if (item) {
  51. item.value = item.initalTransform
  52. ? item.initalTransform(value)
  53. : value
  54. item.valueProp = item.valueProp || 'value'
  55. res[newKey] = item
  56. }
  57. return res
  58. },
  59. //Required将可选变成必选
  60. {} as Required<PropsToForms>,
  61. )
  62. })
  63. return { finalPros }
  64. },
  65. })
  66. </script>
  67. ...

image.png