# 【方案】-海报编辑器

# 前言

在工作中一共实现过两款编辑器的需求,一个是可以自动编辑小程序的界面以及对应功能,一个是可以自己编辑生成一张一张推广的海报,两个需求的底部实现逻辑都比较类似于vdom,用设计好的对象结构来表示整个dom的存在,我们可以修改这个对象结构里面的值达到修改dom样式文字点击逻辑等功能

# 基础底层结构搭建

首先我们来看下大致需要实现的设计图

An image

总体分为<左侧选择组件区域> <中间编辑ui区域> <右侧编辑组件区域>

我们来看下底层设计的数据结构

  • 左侧选择组件区域



 

























const leftComponents = [
 {
    title: '文本模块', //对应的大标题
    showModules: ['XXX', 'YYY'], // 对应展示的权限控制
    isShowComponents: true, // 是否默认展开菜单
    components: [
        {
            type: 'text', // 组件的类型
            name: '小区名称', // 标题
            label: '华业玫瑰四季馨…', // 副标题
            value: 'communityName', // 点击之后界面展示对应的组件名
            style: {}, // 默认样式
            showModules: ['XXX'], // 对应展示的权限控制
        },
        {
            type: 'text',
            name: '亮点标签',
            label: '近地铁 学位房…',
            value: 'tags',
            style: {},
            showModules: ['XXX'],
        },
    ]
 },
 {
     ...
 }
]

我们就可以根据这个数组结构渲染出左侧的组件菜单模块了

  • 中间编辑ui区域

中间可以编辑的ui区域对应着以下对象结构

componentsArr 表示当前选择全部组件的数组




 







componentsArr = [
    {
        index: "1573808540762309657",
        name: "communityName",
    },
    {
        index: "1573808540762306865",
        name: "tag",
    },
]

我们点击左侧组件选择区域的时候会往这个数组插入一条数据,其中index是当前组件的唯一标识,name是当前组件的名字,我们可以根据这个数组在页面中渲染出全部组件的位置以及样式




 









  addComponents(value) { //点击左侧组件菜单添加组件
    const { components, componentsArr } = this.state
    let index = this.createComponentIndex() //生成组件唯一标识
    components[index] = _.cloneDeep(middleComponents[value][value])
    components[index]['index'] = index
    componentsArr.push({
      index,
      name: value,
    })
    this.setState({ components, componentsArr }) // 更新数组数据
    this.pushHistory() // 把当前操作推入操作记录
  }

我们再来看下components这个数据结构,表示全部组件的集合的数据结构,我们也是直接将这个数据结构入库,然后传递给M站网页端




 























components: {
    0: {
        name: "container",
        type: "box",
        content: "",
        from: "container",
        style: {...}, //存放样式
        index: "0",
        alpha: 100,
        backgroundColorAlpha: 100,
    },
    1573808540762309657: {
        name: "communityName"
        type: "text"
        content: "$community_name$"
        from: "communityName1"
        isEdit: "false"
        style: {...}
        maxLength: "20"
        index: "1573808540762309657"
        colorNumber: "000"
        alpha: 100
        backgroundColorAlpha: 100
        numberLineHeight: 33
    }
}

最后我们说下selectedComponents,他表示我们点击中间ui编辑区域时候所选中的组件,之所以设计为一个数组是为了兼容多选组件的需求




 













selectedComponents: [
    {
        name: "communityName"
        type: "text"
        content: "$community_name$"
        from: "communityName1"
        isEdit: "false"
        style: {...}
        maxLength: "20"
        index: "1573808540762309657"
        colorNumber: "000"
        alpha: 100
        backgroundColorAlpha: 100
        numberLineHeight: 33
    }
]

底层的数据结构最主要的就这些了

# 修改选中组件样式

我们在中间ui编辑区域选择组件的时候,会把选中组件的数据结构存在selectedComponents中,右侧修改区域其实就是对selectedComponents这个结构里面的数据进行编辑,如对选中组件的颜色的编辑就是修改对应组件的style.color的值,所以我们可以封装一些方法




 




styleChange(name, e) {
    const { selectedComponent } = this.state
    selectedComponent[xxx].style[name] = e.target.value
    this.setState({
        selectedComponent
    })
}

# 移动选中的组件

我们选中组件的时候可以获取到当前组件的样式,也就是topleftwidthheight这样,我们可以根据这些样式画出一个包围这个组件的辅助线




 

























































  setStyleAssist(component, components) {
    let { width, height, left, top } = component.style
    let [right, bottom] = [0, 0]
    const { selectedComponentsObj } = this.state

    if (components.length > 1) {
      const selectedComponentsObjStyle = { width, height, left, top }

      components.forEach((item) => {
        const { style } = item
        if (style.left < left) {
          left = parseInt(style.left, 10) // 计算出辅助框的最大左边界
        }
        if (style.top < top) {
          top = parseInt(style.top, 10)
        }
        if (style.left + style.width > right) {
          right = parseInt(style.left, 10) + parseInt(style.width, 10)
        }
        if (style.top + style.height > bottom) {
          bottom = parseInt(style.top, 10) + parseInt(style.height, 10)
        }
        if (style.width !== selectedComponentsObjStyle.width) {
          selectedComponentsObjStyle.width = ''
        }
        if (style.height !== selectedComponentsObjStyle.height) {
          selectedComponentsObjStyle.height = ''
        }
        if (style.left !== selectedComponentsObjStyle.left) {
          selectedComponentsObjStyle.left = ''
        }
        if (style.top !== selectedComponentsObjStyle.top) {
          selectedComponentsObjStyle.top = ''
        }
      })
      width = right - left
      height = bottom - top

      components.forEach((item) => { // 上面得出辅助框的宽高左上值,并在这计算所有被选中的组件的所占比例
        const { style } = item
        item.stylePercent = {
          widthPercent: (style.width / width).toFixed(3),
          heightPercent: (style.height / height).toFixed(3),
          leftPercent: ((style.left - left) / width).toFixed(3),
          topPercent: ((style.top - top) / height).toFixed(3),
        }
      })
      selectedComponentsObj.style = selectedComponentsObjStyle
    }

    this.setState({
      styleAssist: {
        width,
        height,
        left,
        top,
      },
      selectedComponentsObj,
    })
  }

我们移动其实就是对画出的辅助线矩形进行移动,然后动态的修改selectedComponentsObj的值更新移动组件的style中的样式就好,进行多选组件也是一样的道理,我们可以遍历全部选择到的组件,取出宽高的和以及lefttop的最大值就好了

# 前进和后退

这里说下实现思路,我们维护一个操作历史的数组,然后每一个步骤例如添加一个组件,删除一个组件,修改组件的样式的时候都会往这个数组里面推入一系列的结构,然后我们就可以对这个数组进行进出栈操作实现操作历史的前进和后退

# 实现单位转换

我们需要把一些颜色单位如#fff啥的和RGBA之间互相转化,还要引入透明度的这个可编辑,这里记录一下单位转换的函数




 
































  getRgbaColor(color, alpha) {
    if (~color.indexOf('rgba')) {
      const alphaBefore = color.match(/\,\s([\d\.]+)\)/)[1]
      return color.replace(alphaBefore, alpha / 100)
    }

    const colorArr = color.split('')
    if (colorArr.includes('#')) {
      colorArr.shift()
    }
    if (colorArr.length !== 3 && colorArr.length !== 6) {
      return
    }
    if (colorArr.length === 3) {
      colorArr.splice(2, 0, colorArr[2])
      colorArr.splice(1, 0, colorArr[1])
      colorArr.splice(0, 0, colorArr[0])
    }
    const arr = []
    for(let i = 0, index = 0; i < 6; i += 2, index++) {
      arr[index] = parseInt(colorArr.slice(i, i + 2).join(''), 16)
    }
    return `rgba(${arr[0]}, ${arr[1]}, ${arr[2]}, ${alpha / 100})`
  }

  getHexColor(color) {
    if (!color) {
      return ''
    }
    if (~color.indexOf('#')) {
      return color.slice(1)
    }
    const arr = color.match(/\d+/g)
    return `${arr[0].toString(16)}${arr[1].toString(16)}${arr[2].toString(16)}`
  }

# 实现截取封面图

因为我们是对一张张海报进行编辑,然后我们需要截取当前中间ui编辑区域作为我们海报的封面图,我们采用html2canvas对封面图进行截取




 



















  uploadPreviewImg() {
    return new Promise((resolve, reject) => {
      html2canvas(document.querySelector('.middle-block'), {
        allowTaint: true
      }).then(canvas => {
        if (canvas.toBlob) {
          canvas.toBlob((blob) => {
            upload(blob).then((res) => {
              const name = _.get(res, 'data[0].name', '')
              if (name) {
                resolve(name)
              } else {
                reject('上传海报封面图失败')
              }
            })
          }, 'image/jpeg', 0.2)
        } else {
          reject('当前浏览器不能生成海报预览图,请换成最新版的谷歌浏览器或者Safari')
        }
      })
    })
  }

# 未完待续