web架构师编辑器内容-创建业务组件和编辑器基本行为

编辑器主要分为三部分,左侧是组件模板库,中间是画布区域,右侧是面板设置区域。

左侧是预设各种组件模板进行添加

中间是使用交互手段来更新元素的值

右侧是使用表单的方式来更新元素的值。

大致效果:

  1. 左侧组件模板库
    最初的模板配置:
js 复制代码
export const defaultTextTemplates = [
  {
    text: '大标题',
    fontSize: '30px',
    fontWeight: 'bold',
    tag: 'h2'
  },
  {
    text: '楷体副标题',
    fontSize: '20px',
    fontWeight: 'bold',
    fontFamily: '"KaiTi","STKaiti"',
    tag: 'h2'
  },
  {
    text: '正文内容',
    tag: 'p'
  },
  {
    text: '宋体正文内容',
    tag: 'p',
    fontFamily: '"SimSun","STSong"'
  },
  {
    text: 'Arial style',
    tag: 'p',
    fontFamily: '"Arial", sans-serif'
  },
  {
    text: 'Comic Sans',
    tag: 'p',
    fontFamily: '"Comic Sans MS"'
  },
  {
    text: 'Courier New',
    tag: 'p',
    fontFamily: '"Courier New", monospace'
  },
  {
    text: 'Times New Roman',
    tag: 'p',
    fontFamily: '"Times New Roman", serif'
  },
  {
    text: '链接内容',
    color: '#1890ff',
    textDecoration: 'underline',
    tag: 'p'
  },
  {
    text: '按钮内容',
    color: '#ffffff',
    backgroundColor: '#1890ff',
    borderWidth: '1px',
    borderColor: '#1890ff',
    borderStyle: 'solid',
    borderRadius: '2px',
    paddingLeft: '10px',
    paddingRight: '10px',
    paddingTop: '5px',
    paddingBottom: '5px',
    width: '100px',
    tag: 'button',
    textAlign: 'center'
  }
]

在component-list组件中循环渲染这个模板

compnent-list组件:

js 复制代码
<div
  class="component-item"
  v-for="(item, index) in props.list"
  @click="onItemClick(item)"
  :key="index"
>
  <LText v-bind="item"></LText>
</div>

// LText组件
<component class="l-text-component" :is="props.tag" :style="styleProps" @click="handleClick">{{ props.text }}
</component>
  1. 中间画布区
    基本的数据结构
js 复制代码
export interface ComponentData {
  props: { [key: string]: any }
  id: string
  name: string
}

在左侧模板区域点击的时候,会emit一个onItemCreated事件:

js 复制代码
const onItemCreated = (props: ComponentData) => {
  store.commit('addComponent', props)
}

store里面的addComponent方法:

js 复制代码
addComponent(state, props) {
  const newComponent: ComponentData =
    id: uuidv4(),
    name: 'l-text',
    props
  }
  state.components.push(newComponent)
},

渲染中间画布区域:

js 复制代码
<div v-for="component in components" :key="component.id">
    <EditWrapper v-if="!component.isHidden"
      :id="component.id"
      @set-active="setActive"
      :active="component.id === (currentElement && currentElement.id)" :props="component.props"
    >
    <component :is="canvasComponentList[component.name as 'l-text' | 'l-image' | 'l-shape']" v-bind="component.props" :isEditing="true"/>
  </EditWrapper>
  </div>

editWrapper组件就是为了隔离两个组件,方便后续的一些拖拽,拉伸,吸附的一些效果。

js 复制代码
<template>
<div class="edit-wrapper" @click="itemClick"
    @dblclick="itemEdit"
    ref="editWrapper"
    :class="{active: active}" :style="styleProps"
    :data-component-id="id"
> 
  <!-- 元素的扩大 -->
  <div class="move-wrapper" ref="moveWrapper" @mousedown="startMove">
    <slot></slot>
  </div>
  <div class='resizers'>
    <div class='resizer top-left' @mousedown="startResize($event, 'top-left')"></div>
    <div class='resizer top-right'  @mousedown="startResize($event, 'top-right')"></div>
    <div class='resizer bottom-left' @mousedown="startResize($event, 'bottom-left')"></div>
    <div class='resizer bottom-right' @mousedown="startResize($event, 'bottom-right')"></div>
  </div>
</div>
</template>
  1. 右侧设置面板区域的渲染:
    在中间画布区域进行点击的时候,通过setActive事件,我们可以拿到当前的元素,
js 复制代码
// store中的setActive
setActive(state, currentId: string) {
  state.currentElement = currentId;
},

然后就可以通过props-table组件进行渲染了:

js 复制代码
<PropsTable v-if="currentElement && currentElement.props" 
  :props="currentElement.props"
  @change="handleChange"
></PropsTable>

props-table比较麻烦我们来一一讲解,首先来看一下props-talbe的template部分:

js 复制代码
<template>
  <div class="props-table">
    <div
      v-for="(value, key) in finalProps"
      :key="key"
      :class="{ 'no-text': !value.text }"
      class="prop-item"
      :id="`item-${key}`"
    >
      <span class="label" v-if="value.text">{{ value.text }}</span>
      <div :class="`prop-component component-${value.component}`">
        <component
          :is="value.component"
          :[value.valueProp]="value.value"
          v-bind="value.extraProps"
          v-on="value.events"
        >
          <template v-if="value.options">
            <component
              :is="value.subComponent"
              v-for="(option, k) in value.options"
              :key="k"
              :value="option.value"
            >
              <render-vnode :vNode="option.text"></render-vnode>
            </component>
          </template>
        </component>
      </div>
    </div>
  </div>
</template>

我们最终渲染的是finalProps这个数据,finalProps数据的生成:

js 复制代码
// 属性转化成表单的映射表 key:属性  value:使用的组件
export const mapPropsToForms: PropsToForms = {
  // 比如: text 属性,使用 a-input 这个组件去编辑
  text: {
    text: '文本',
    component: 'a-input',
    afterTransform: (e: any) => e.target.value,
  },
  fontSize: {
    text: '字号',
    component: 'a-input-number',
    // 为了适配类型,进行一定的转换
    initalTransform: (v: string) => parseInt(v),
    afterTransform: (e: number) => e ? `${e}px` : '',
  },
  lineHeight: {
    text: '行高',
    component: 'a-slider',
    extraProps: {
      min: 0,
      max: 3,
      step: 0.1
    },
    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: '右'
      }
    ],
    afterTransform: (e: any) => e.target.value
  },
  fontFamily: {
    component: 'a-select',
    subComponent: 'a-select-option',
    text: '字体',
    options: [
      {
        value: '',
        text: '无'
      },
      ...fontFamilyOptions
    ],
    afterTransform: (e: any) => e
  },
  color: {
    component: 'color-pick',
    text: '字体颜色',
    afterTransform: (e: any) => e
  }
}
const finalProps = computed(() => {
  // reduce是使用loadsh里面的
  return reduce(
    props.props,
    (result, value, key) => {
      const newKey = key as keyof AllComponentProps;
      const item = mapPropsToForms[newKey];
      if (item) {
       // v-model默认绑定的值,是value,可以自定义
       // v-model双向数据绑定的事件,默认是change事件,也可以自定义
       // initalTransform编辑前的value转换,为了适配类型,进行一定的转换
       // afterTransform 处理上双向数据绑定后的值。
        const {
          valueProp = 'value',
          eventName = 'change',
          initalTransform,
          afterTransform,
        } = item;
        const newItem: FormProps = {
          ...item,
          value: initalTransform ? initalTransform(value) : value,
          valueProp,
          eventName,
          events: {
            [eventName]: (e: any) => {
              context.emit('change', {
                key,
                value: afterTransform ? afterTransform(e) : e,
              });
            },
          },
        };
        result[newKey] = newItem;
      }
      return result;
    },
    {} as { [key: string]: FormProps }
  );
});

我们传递的props值是这样的:

最终转换成出来的值是这样的

当组件内的change事件改变后,组件内部会触发

js 复制代码
context.emit('change', { key, value: afterTransform ? afterTransform(e) : e,});

在父组件中接收change事件来改变stroe中的compoents的值

js 复制代码
const handleChange = (e) => {
  console.log('event', e);
  store.commit('updateComponent', e)
}

在store中改变components属性

js 复制代码
updateComponent(state, { id, key, value, isProps}) {
  const updatedComponent = state.components.find((component) => component.id === (id || state.currentElement)) as any
  if(updatedComponent) {
    updatedComponent.props[key as keyof TextComponentProps] = value;
  }
}

难点:

相关推荐
崔庆才丨静觅8 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60619 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了9 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅9 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅10 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅10 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment10 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅10 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊10 小时前
jwt介绍
前端
爱敲代码的小鱼11 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax