手把手教你打造海报设计器

原创
2022/04/29 11:32
阅读数 75

最近有个项目中要用做营销管理,营销管理中一个重要的环节就是活动海报的diy制作。一般情况下,出现这种需求,第一时间就是到社区去看看,有咱就白嫖多香。但是,事与愿违。找遍了社区,我只发现一个叫 fast-poster 的项目,但是他们开源的版本,前端加了密,无法自由更改。付费的版本确实可以改动ui,但是因为版权问题,也无法在实际的使用版本提供给客户这部分的源码。这个限制虽然很合理,但是跟我们的产品有一些冲突,因为我们要源码供应。所以我就决定依托于他的ui和布局的样式,仿一个出来。将他以MIT协议的形式开源,开放给大家使用。由于没有源码参考,我只能以我有限的前端经验(话说我是个后端来着...),去完成他。好了废话不多说,开始今天的实现之旅。

1、技术选型

首先来最主要的是前端的技术选型。由于我接触的比较多的是 elementui 所以这里我选择了 vue + elementui 的组合方式,来进行整体的布局。这里还有个比较重要的环节,就是拖拽和缩放。关于拖拽和缩放的组件,我之前只解除过拖拽的,缩放的倒是没有。于是我在github上搜索这方面的组件库。被我发现了几个 gorkys / vue-draggable-resizable-gorkysmauricius / vue-draggable-resizable 由于vue-draggable-resizable-gorkys 是基于 vue-draggable-resizable 二次开发的产物,因此他们的基础逻辑是一致的,导致我最初用这个组件去做的时候,遇到了背景缩放和他们用于定位的属性translate 出现了严重的冲突,当然我觉得这个问题应该是可以解决的,但是由于我前端不是很好,所以只能选择其他的组件解决这个问题。于是我又开始寻找,最终找到了kirillmurashov/vue-drag-resize,这个库的定位方式与他们不同,所以不会出现那个问题。这个最重要的组件解决了,接下里就是二维码的qrcode组件了,这里我选择了qrcodejsqrcodejs2 (话说这两个为啥在一起,我也不是很了解,我看的资料都是他们在一起安装的。)总结一下

框架 用途
vue-drag-resize 用于组件的拖拽和缩放
qrcodejs、qrcodejs2 用于二维码的生成
elementui 用于总体的布局和样式图标等

vue这里我选择的是 2.x 版本,别问我为啥不用 3.x,因我接触的 2.x 较多。接下来进入主题。

2、整体布局

image.png

总体的样式还是参考了市面上的成熟产品,做了简答的布局处理。整体的功能区域,接下来会一一讲解,大体的布局是基于 layout 布局,这个你可以根据自己的实际使用,进行左右比例的修改。

3、元素的确定

海报中最重要的几个部分是背景文字二维码图片 这些。因此,我来定义一个整体的对象来保存这些需要用到的东西,以此方便记录我们的海报整体的设计信息。先来定义元元素(这里我说的元元素,就是 文字二维码图片

// 基础素材
material: {
    text: {
      a: 'left', // 对齐方式
      warp: false, // 是否开启换行
      t: 'text', // 类型
      c: '#000', // 颜色
      bgc: '', // 背景色
      h: 200, // 高度
      w: 200, // 宽度
      x: 100, // x坐标
      y: 100, // y坐标
      fn: 'ali-Regular', // 字体
      s: 68, // 字体大小
      name: '文字', // 名称
      v: '内容', // 内容
      rm: '备注', // 备注
      uuid: '' // 唯一标识
    },
    qrcode: {
      t: 'qrcode', // 类型
      c: '#000000', // 颜色
      h: 150, // 高度
      w: 150, // 宽度
      x: 250, // x坐标
      y: 400, // y坐标
      fn: '', // 字体
      bgc: '#ffffff', // 背景色
      name: '文字', // 名称
      v: 'https://gitee.com/nickbai/freeposter', // 内容
      rm: '备注', // 备注
      uuid: '' // 唯一标识
    },
    img: {
      t: 'image', // 类型
      h: 51, // 高度
      w: 163, // 宽度
      x: 67, // x坐标
      y: 57, // y坐标
      c: '#000000', // 颜色
      bgc: '#fff', // 背景色
      fn: '', // 字体
      name: '图片', // 名称
      v: 'https://poster.prodapi.cn/static/images/xiaoniu.png', // 内容
      rm: '备注', // 备注
      uuid: '' // 唯一标识
    }
},

这样一个海报中,无论增加了多少个 文字、图片、二维码 那我们新增的都是这些元元素,我们只要记录这些元素在整个背景中的 位置、大小、内容、颜色等等信息,就可以将这些元素最终合并成一个完整的海报。

有了元素的定义了,这里我们也把海报虚拟成一个对象来承载整体的信息。

formData: {
    name: '',
    type: 'jpg',
    qulity: 80,
    width: 750,
    height: 1333,
    activeName: 'img',
    color: '#fff',
    img_src: '',
    item: []
}

这里有两个比较重要的属性img_src 用于存放海报的封面,item 是一个数组,用来盛放在封面上的元元素(文字、海报、图片)。

4、海报的设计和制作

我们元素都有了之后,每个海报对象都用一个formData 去表示,这样当我们点击菜单区的 海报封面元素 的时候,只要将对应的对象添加到 formData中便可以实时记录整个海报的信息了。我们看 diy方法

// diy设计
async diy (id) {
  const res = await posterApi.info({ id: id })
  // 基础信息
  this.formData = JSON.parse(res.data.design_content)
  this.formData.qulity = parseInt(this.formData.qulity)

  this.formData.item.forEach((ele, index) => {
    if (ele.t === 'qrcode') {
      this.$nextTick(() => {
        this.qrcode(index, ele)
      })
    }
  })
},

当我们点击海报菜单的时候,我们根据整个海报的id去后台拿到他完整的 formData数据

{"code":0,"data":[{"id":2,"name":"活动4","preview":"http:\/\/www.posterapi.com\/poster_preview\/cover\/626aa131d72cd.jpeg","status":1,"design_content":"{\"name\":\"\\u6d3b\\u52a84\",\"type\":\"jpeg\",\"qulity\":80,\"width\":750,\"height\":1200,\"activeName\":\"img\",\"color\":\"#fff\",\"img_src\":\"http:\\\/\\\/www.posterapi.com\\\/storage\\\/20220428\\\/06bad084bfb20fd3df35c3131e2581a9.jpeg\",\"item\":[{\"t\":\"image\",\"h\":51,\"w\":163,\"x\":544,\"y\":58,\"c\":\"#000000\",\"bgc\":\"#fff\",\"fn\":\"\",\"name\":\"\\u56fe\\u7247\",\"v\":\"https:\\\/\\\/www.huocms.com\\\/static\\\/home\\\/images\\\/logo.png\",\"rm\":\"\\u5907\\u6ce8\",\"uuid\":\"1b5c2704\"},{\"t\":\"qrcode\",\"c\":\"#000000\",\"h\":150,\"w\":150,\"x\":162,\"y\":868,\"fn\":\"\",\"bgc\":\"#ffffff\",\"name\":\"\\u6587\\u5b57\",\"v\":\"https:\\\/\\\/www.huocms.com\",\"rm\":\"\\u5907\\u6ce8\",\"uuid\":\"2547f9a3\"}]}","create_time":"2022-04-28 22:14:10","update_time":null}],"msg":"success"}

设计区根据拥有的属性,进行分批渲染

<el-col :span="13" class="poster-view">
  <div class="free-editor-wrapper" :style="wrapper_style">
    <div ref="poster" class="free-poster-bg" :style="bg_style">
      <VueDragResize
        v-for="(item, index) in formData.item"
        :key="item.uuid"
        :w="calcSize(item.w)"
        :h="calcSize(item.h)"
        :x="calcSize(item.x)"
        :y="calcSize(item.y)"
        :is-active="index === nowFormIndex"
        @dragging="dragging"
        @resizing="resizing"
        @activated="onActivated(index)"
        @deactivated="onDeactivated(index)"
      >
        <div
          v-if="item.t === 'text'"
          :style="{
            width: '100%',
            height: '100%',
            'font-size': calcSize(item.s) + 'px',
            'font-family': item.fn,
            color: item.c,
            //'text-align': item.a,
            //'white-space': item.warp ? 'break-spaces' : 'pre',
            'line-height': 'calc(1em + 4px)'
          }"
        >
          {{ item.v }}
        </div>
        <img
          v-if="item.t === 'image'"
          :src="item.v"
          style="width: 100%; height: 100%"
        />
        <div
          v-if="item.t === 'qrcode'"
          :id="'qrcode' + index"
          :ref="'qrcode' + index"
          style="width: 100%; height: 100%"
        />
      </VueDragResize>
    </div>
  </div>
  <div class="free-editor-bottom-bar">
    <i class="el-icon-remove" @click="changeSize(1)" />
    <span class="scale-num">{{ resize }}%</span>
    <i class="el-icon-circle-plus" @click="changeSize(2)" />
  </div>
</el-col>

这里我们把每个元素用 VueDragResize 包住,这样每个组件都拥有了可拖拽和缩放的功能。
我们监听 dragging 拖拽事件,可以得知此时该组件的 x,y坐标。
我们监听 resizing 改变大小事件,可以得知该组件的width,height 值。
我们监听 activated 激活事件用于区分当前是哪个组件处于选中状态,方便我们从 formData.item 中检索出该组件的属性数据,从而让我们拥有改变和设置组件的功能。 在循环输出 formData.item 中的组件的时候,我们来判断类型t 依次来渲染不同的 元素 在背景上。
这样整个海报就完整的以html的形式展现在大家眼前了。

5、撤销、删除、预览、保存的实现

撤销功能如何实现?

我们每次点击菜单组件里面的素材的时候,都会调用addItem方法

// 添加组件
addItem(type) {
  if (type == 'text') {
    let item = JSON.parse(JSON.stringify(this.material.text))
    item.uuid = this.uuid()
    this.formData.item.push(item)
  } else if (type == 'image') {
    let item = JSON.parse(JSON.stringify(this.material.img))
    item.uuid = this.uuid()
    this.formData.item.push(item)
  } else if (type == 'qrcode') {
    let item = JSON.parse(JSON.stringify(this.material.qrcode))
    item.uuid = this.uuid()
    let index = this.formData.item.length
    this.formData.item.push(item)

    this.$nextTick(() => {
      this.qrcode(index, item)
    })
  }
},

该方法用于拷贝元元素对象到当前formData.item 数组中,因此撤销的思路就来了,由于我们是通过push的方式追加到数组中的,因此我们只需要每次点击的时候,从 formData.item 数组中删除最后一个元素即可

// 撤销
back() {
  let length = this.formData.item.length
  if (length) {
    this.formData.item.pop()
  }
},

删除功能如何实现?

跟撤销差不多的原理,我们通过activated 可获得当前点击的 元素 因此,我们只要在 formData.item 中找到他,并把它删除即可。

// 删除组件
delItem() {
  this.formData.item.splice(this.nowFormIndex, 1)
},

预览功能如何实现

这里我用到了后端合成的方案,为啥没选用js的 html2canvas ,我也尝试过,但是感觉小效果不佳。当然你也可以喷我技术菜,于是我选用了成熟的后端合成方案。我们只要将 formData 这个对象序列化还给后端,后端解析合成即可。

// 预览
async preview () {
  console.log(this.formData)
  const res = await posterApi.preview(this.formData)
  if (res.code === 0) {
    this.previewImg = true
    this.url = res.data
    this.srcList[0] = res.data
  } else {
    this.$message.error(res.msg)
  }
},

关于后端如何合成的,可以参考 https://gitee.com/nickbai/posterapi

最后

由于篇幅有限,我只讲了我认为比较主要的内容,当然你如果有其他需要关注的细节,可以移步到 https://gitee.com/nickbai/freeposter 别忘了给我点一个

展开阅读全文
加载中
点击引领话题📣 发布并加入讨论🔥
0 评论
0 收藏
0
分享
返回顶部
顶部