最近有个项目中要用做营销管理,营销管理中一个重要的环节就是活动海报的diy制作。一般情况下,出现这种需求,第一时间就是到社区去看看,有咱就白嫖多香。但是,事与愿违。找遍了社区,我只发现一个叫 fast-poster
的项目,但是他们开源的版本,前端加了密,无法自由更改。付费的版本确实可以改动ui,但是因为版权问题,也无法在实际的使用版本提供给客户这部分的源码。这个限制虽然很合理,但是跟我们的产品有一些冲突,因为我们要源码供应。所以我就决定依托于他的ui和布局的样式,仿一个出来。将他以MIT
协议的形式开源,开放给大家使用。由于没有源码参考,我只能以我有限的前端经验(话说我是个后端来着...),去完成他。好了废话不多说,开始今天的实现之旅。
1、技术选型
首先来最主要的是前端的技术选型。由于我接触的比较多的是 elementui
所以这里我选择了 vue + elementui
的组合方式,来进行整体的布局。这里还有个比较重要的环节,就是拖拽和缩放。关于拖拽和缩放的组件,我之前只解除过拖拽的,缩放的倒是没有。于是我在github上搜索这方面的组件库。被我发现了几个 gorkys / vue-draggable-resizable-gorkys 和 mauricius / vue-draggable-resizable 由于vue-draggable-resizable-gorkys 是基于 vue-draggable-resizable 二次开发的产物,因此他们的基础逻辑是一致的,导致我最初用这个组件去做的时候,遇到了背景缩放和他们用于定位的属性translate
出现了严重的冲突,当然我觉得这个问题应该是可以解决的,但是由于我前端不是很好,所以只能选择其他的组件解决这个问题。于是我又开始寻找,最终找到了kirillmurashov/vue-drag-resize,这个库的定位方式与他们不同,所以不会出现那个问题。这个最重要的组件解决了,接下里就是二维码的qrcode组件了,这里我选择了qrcodejs
和qrcodejs2
(话说这两个为啥在一起,我也不是很了解,我看的资料都是他们在一起安装的。)总结一下
框架 | 用途 |
---|---|
vue-drag-resize | 用于组件的拖拽和缩放 |
qrcodejs、qrcodejs2 | 用于二维码的生成 |
elementui | 用于总体的布局和样式图标等 |
vue这里我选择的是 2.x 版本,别问我为啥不用 3.x,因我接触的 2.x 较多。接下来进入主题。
2、整体布局
总体的样式还是参考了市面上的成熟产品,做了简答的布局处理。整体的功能区域,接下来会一一讲解,大体的布局是基于 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 别忘了给我点一个赞
。