要求能够实现将个人照片、信息,展示到小程序页面中,并可以导出。但是因为后端没有一个单独的图片服务器,并且图片是以一个blob对象捎带在响应数据中的,因此还要考虑blob数据转换成文件(图片)。
可以拆解一下功能逻辑:
1、发起请求,保存图片到本地路径,或者本地临时路径。
2、绘制。
3、绑定按键,点击即可导出文件(图片)。
其中1、3都有uniapp和小程序都有自带的api。
2的话主要是考虑canvas,主要有两种实现思路,一种是通过wxml导出,一种是直接在canvas上绘制。其中wxml导出是不大好实现的,通过查资料发现了有wxml2canvas这样的插件,可以阉割地实现这样一个功能。
下面分步来实现功能:
1、请求数据,这边比较简单,需要注意的就是同步异步的问题,这里处理地比较简单,用了await强制整个过程都是同步的。
写入文件:首先将blob这种二进制流放到一个缓冲区,然后写到一个路径,这里用到了一个小程序的缓存,然后同时做了一个随机文件名的处理,防止命名的碰撞。注意这里当头像数据存储完毕后,直接开始画了头像,是因为这里我发现wxml2canvas只有对text类型的节点处理的比较好,其他类型(图片、矩形)等处理起来不如直接用canvas快,所以这里图片和文本是分开做的,图片是用的原生canvas绘制(因为实现起来比较简单,所以这里也不会过多提及),文本是依靠wxml来实现的。
async getData() {
await uni.request({
url: 'http://localhost:8081/teacher/selectTeacherElectronicResumeInfo',
data: {
tea_id: this.tea_id
},
method: 'POST'
}).then(res => {
this.resumeInfo = res[1].data.data
})
await uni.request({
url: 'http://localhost:8081/teacher/findTeaPhoto',
data: {
tea_id: this.tea_id
},
method: 'POST'
}).then(async res => {
this.img = uni.base64ToArrayBuffer(res[1].data.data)
var save = wx.getFileSystemManager()
var rand = Math.random()
this.path = wx.env.USER_DATA_PATH + `/pic${Math.random()}.png`
console.log(this.path)
console.log('tempPath->', this.path)
await save.writeFile({
filePath: this.path,
data: this.img,
encoding: 'binary',
success: (res) => {
console.log('success')
this.drawAvatar()
},
fail: () => {
console.log('fail')
},
complete: () => {
// console.log('complete')
}
})
})
}
2、绘制
通过1,知道图片是单独使用原生canvas实现的,所以绘制部分只需要使用wxml以及wxml2canvas导出即可,这里贴一下各部分的代码。
通过代码也可以看到实现的是比较粗糙,线条是通过破折号来实现的。
<template>
<view>
<view class="share__canvas share__canvas1 canvas" id="wxml">
<view class="content-box-first">
<view class="draw_canvas title" data-type="text" data-text="个人信息">个人信息</view>
<view class="draw_canvas" data-type="text" :data-text="`姓名:${resumeInfo.tea.teaName}`">{{resumeInfo.tea.teaName}}</view>
<view class="draw_canvas" data-type="text" :data-text="`性别:${resumeInfo.tea.teaGender}`">{{resumeInfo.tea.teaGender}}</view>
<view class="draw_canvas" data-type="text" :data-text="`电子邮箱:${resumeInfo.tea.mailAddress}`">{{resumeInfo.tea.mailAddress}}</view>
<view class="draw_canvas" data-type="text" :data-text="`联系电话:${resumeInfo.tea.teaTelNum}`">{{resumeInfo.tea.teaTelNum}}</view>
<view class="draw_canvas" data-type="text" :data-text="`研究方向:${resumeInfo.tea.searchDirection}`">{{resumeInfo.tea.searchDirection}}</view>
<view class="draw_canvas" data-type="text" :data-text="`所属学院:${resumeInfo.tea.teaDname}`">{{resumeInfo.tea.teaDname}}</view>
<view class="draw_canvas" data-type="text" :data-text="`学历:${resumeInfo.tea.highestEdu}`">{{resumeInfo.tea.highestEdu}}</view>
<view class="draw_canvas" data-type="text" :data-text="`职称:${resumeInfo.tea.position}`">{{resumeInfo.tea.position}}</view>
</view>
<view class="draw_canvas line" data-text="—————————————————————" data-type="text"></view>
<view class="content-box-other">
<view class="draw_canvas title" data-type="text" data-text="获得奖项">获得奖项</view>
<view v-for="(item,index) in resumeInfo.award" :key="index">
<view class="draw_canvas" data-type="text" :data-text="`${item.awardName}-${item.awardSetDep}`">{{`${item.awardName}-${item.awardSetDep}`}}</view>
</view>
</view>
<view class="draw_canvas line" data-text="—————————————————————" data-type="text"></view>
<view class="content-box-other">
<view class="draw_canvas title" data-type="text" data-text="主持项目">主持项目</view>
<view v-for="(item,index) in resumeInfo.project" :key="index">
<view class="draw_canvas long-text" data-type="text" :data-text="`${item.proName}-${item.proGrade}`">{{`${item.proName}-${item.proGrade}`}}</view>
</view>
</view>
<view class="draw_canvas line" data-text="—————————————————————" data-type="text"></view>
<view class="content-box-other">
<view class="draw_canvas title" data-type="text" data-text="发表论文">发表论文</view>
<view v-for="(item,index) in resumeInfo.paper" :key="index">
<view class="draw_canvas long-text" data-type="text" :data-text="`${item.paperTitle}-${item.colMethod}`">{{`${item.paperTitle}-${item.colMethod}`}}</view>
</view>
</view>
<view class="draw_canvas line" data-text="—————————————————————" data-type="text"></view>
<view class="content-box-other">
<view class="draw_canvas title" data-type="text" data-text="主持项目">主持项目</view>
<view v-for="(item,index) in resumeInfo.project" :key="index">
<view class="draw_canvas long-text" data-type="text" :data-text="`${item.proName}-${item.proGrade}`">{{`${item.proName}-${item.proGrade}`}}</view>
</view>
</view>
<view class="draw_canvas line" data-text="—————————————————————" data-type="text"></view>
<view class="content-box-other">
<view class="draw_canvas title" data-type="text" data-text="学术专著">学术专著</view>
<view v-for="(item,index) in resumeInfo.book" :key="index">
<view class="draw_canvas" data-type="text" :data-text="`《${item.monographName}》-${item.press}`">{{`《${item.monographName}》-${item.press}`}}</view>
</view>
</view>
<view class="draw_canvas line" data-text="—————————————————————" data-type="text"></view>
<view class="content-box-other">
<view class="draw_canvas title" data-type="text" data-text="发明专利">发明专利</view>
<view v-for="(item,index) in resumeInfo.patent" :key="index">
<view class="draw_canvas" data-type="text" :data-text="`${item.patName}-${item.patType}-${item.state}`">{{`${item.patName}-${item.patType}-${item.state}`}}</view>
</view>
</view>
<view class="draw_canvas line" data-text="—————————————————————" data-type="text"></view>
</view>
<canvas canvas-id="resume" id="resume-box"></canvas>
<button id="footer-btn-save" @click="getResume">生成简历</button>
</view>
</template>
drawResume() {
var systemInfo = uni.getSystemInfoSync()
this.drawCanvasImage = new Wxml2Canvas({
width: systemInfo.screenWidth, // 宽, 以iphone6为基准,传具体数值,其他机型自动适配
height: systemInfo.screenHeight*1.5, // 高
element: 'resume',
background: '#ffffff',
})
let data = {
list: [{
type: 'wxml',
class: '.share__canvas1 .draw_canvas', // draw_canvas指定待绘制的元素
limit: '.share__canvas1', // 限定绘制元素的范围,取指定元素与它的相对位置
x: 0,
y: 0
}]
}
this.drawCanvasImage.draw(data);
}
本来想使用scss的嵌套的特性,结果发现实际上多分出来几个类就行,比如title、item
<style lang="scss">
.line{
font-weight: bolder ;
color: grey;
margin-left: 50rpx;
}
#resume-box {
height: 150vh;
width: 100vw;
top: 0;
}
.long-text{
font-size: 24rpx;
}
.item{
font-size: 28rpx;
color:#303030;
}
.title{
margin-top:20rpx;
font-size: 40rpx;
font-weight: bold;
text-decoration:underline
}
#wxml {
position: fixed;
top: 0;
height: 150vh;
visibility: hidden;
}
#footer-btn-save {
height: 100rpx;
line-height: 100rpx;
}
.content-box-first{
margin-top: 300rpx;
margin-left: 50rpx;
width: 100vh;
}
.content-box-other{
margin-top: 50rpx;
margin-left: 50rpx;
width: 100vh;
}
</style>
3、导出图片
直接调用wx的api即可
getResume() {
wx.canvasToTempFilePath({
canvasId: 'resume',
fileType: 'png',
quality: 1,
success: (res) => {
wx.saveImageToPhotosAlbum({
filePath: res.tempFilePath,
success: () => {
console.log('保存成功')
},
fail: () => {},
complete: () => {}
})
},
fail: () => {
console.log()
},
complete: () => {}
})
}
大致可以实现成这样,实际上如果仔细设计一下ui,可以更好看,但没必要。
这里贴一下完整代码
<template>
<view>
<view class="share__canvas share__canvas1 canvas" id="wxml">
<view class="content-box-first">
<view class="draw_canvas title" data-type="text" data-text="个人信息">个人信息</view>
<view class="draw_canvas" data-type="text" :data-text="`姓名:${resumeInfo.tea.teaName}`">{{resumeInfo.tea.teaName}}</view>
<view class="draw_canvas" data-type="text" :data-text="`性别:${resumeInfo.tea.teaGender}`">{{resumeInfo.tea.teaGender}}</view>
<view class="draw_canvas" data-type="text" :data-text="`电子邮箱:${resumeInfo.tea.mailAddress}`">{{resumeInfo.tea.mailAddress}}</view>
<view class="draw_canvas" data-type="text" :data-text="`联系电话:${resumeInfo.tea.teaTelNum}`">{{resumeInfo.tea.teaTelNum}}</view>
<view class="draw_canvas" data-type="text" :data-text="`研究方向:${resumeInfo.tea.searchDirection}`">{{resumeInfo.tea.searchDirection}}</view>
<view class="draw_canvas" data-type="text" :data-text="`所属学院:${resumeInfo.tea.teaDname}`">{{resumeInfo.tea.teaDname}}</view>
<view class="draw_canvas" data-type="text" :data-text="`学历:${resumeInfo.tea.highestEdu}`">{{resumeInfo.tea.highestEdu}}</view>
<view class="draw_canvas" data-type="text" :data-text="`职称:${resumeInfo.tea.position}`">{{resumeInfo.tea.position}}</view>
</view>
<view class="draw_canvas line" data-text="—————————————————————" data-type="text"></view>
<view class="content-box-other">
<view class="draw_canvas title" data-type="text" data-text="获得奖项">获得奖项</view>
<view v-for="(item,index) in resumeInfo.award" :key="index">
<view class="draw_canvas" data-type="text" :data-text="`${item.awardName}-${item.awardSetDep}`">{{`${item.awardName}-${item.awardSetDep}`}}</view>
</view>
</view>
<view class="draw_canvas line" data-text="—————————————————————" data-type="text"></view>
<view class="content-box-other">
<view class="draw_canvas title" data-type="text" data-text="主持项目">主持项目</view>
<view v-for="(item,index) in resumeInfo.project" :key="index">
<view class="draw_canvas long-text" data-type="text" :data-text="`${item.proName}-${item.proGrade}`">{{`${item.proName}-${item.proGrade}`}}</view>
</view>
</view>
<view class="draw_canvas line" data-text="—————————————————————" data-type="text"></view>
<view class="content-box-other">
<view class="draw_canvas title" data-type="text" data-text="发表论文">发表论文</view>
<view v-for="(item,index) in resumeInfo.paper" :key="index">
<view class="draw_canvas long-text" data-type="text" :data-text="`${item.paperTitle}-${item.colMethod}`">{{`${item.paperTitle}-${item.colMethod}`}}</view>
</view>
</view>
<view class="draw_canvas line" data-text="—————————————————————" data-type="text"></view>
<view class="content-box-other">
<view class="draw_canvas title" data-type="text" data-text="主持项目">主持项目</view>
<view v-for="(item,index) in resumeInfo.project" :key="index">
<view class="draw_canvas long-text" data-type="text" :data-text="`${item.proName}-${item.proGrade}`">{{`${item.proName}-${item.proGrade}`}}</view>
</view>
</view>
<view class="draw_canvas line" data-text="—————————————————————" data-type="text"></view>
<view class="content-box-other">
<view class="draw_canvas title" data-type="text" data-text="学术专著">学术专著</view>
<view v-for="(item,index) in resumeInfo.book" :key="index">
<view class="draw_canvas" data-type="text" :data-text="`《${item.monographName}》-${item.press}`">{{`《${item.monographName}》-${item.press}`}}</view>
</view>
</view>
<view class="draw_canvas line" data-text="—————————————————————" data-type="text"></view>
<view class="content-box-other">
<view class="draw_canvas title" data-type="text" data-text="发明专利">发明专利</view>
<view v-for="(item,index) in resumeInfo.patent" :key="index">
<view class="draw_canvas" data-type="text" :data-text="`${item.patName}-${item.patType}-${item.state}`">{{`${item.patName}-${item.patType}-${item.state}`}}</view>
</view>
</view>
<view class="draw_canvas line" data-text="—————————————————————" data-type="text"></view>
</view>
<canvas canvas-id="resume" id="resume-box"></canvas>
<button id="footer-btn-save" @click="getResume">生成简历</button>
</view>
</template>
<script>
import Wxml2Canvas from 'wxml2canvas'
export default {
data() {
return {
img: '',
imgUrl: '',
width: uni.getSystemInfoSync().screenWidth,
height: uni.getSystemInfoSync().screenHeight,
ctx: uni.createCanvasContext('resume'),
resumeInfo: {},
tea_id: 2007020301
}
},
methods: {
drawResume() {
var systemInfo = uni.getSystemInfoSync()
this.drawCanvasImage = new Wxml2Canvas({
width: systemInfo.screenWidth, // 宽, 以iphone6为基准,传具体数值,其他机型自动适配
height: systemInfo.screenHeight*1.5, // 高
element: 'resume',
background: '#ffffff',
})
let data = {
list: [{
type: 'wxml',
class: '.share__canvas1 .draw_canvas', // draw_canvas指定待绘制的元素
limit: '.share__canvas1', // 限定绘制元素的范围,取指定元素与它的相对位置
x: 0,
y: 0
}]
}
this.drawCanvasImage.draw(data);
},
getResume() {
wx.canvasToTempFilePath({
canvasId: 'resume',
fileType: 'png',
quality: 1,
success: (res) => {
wx.saveImageToPhotosAlbum({
filePath: res.tempFilePath,
success: () => {
console.log('保存成功')
},
fail: () => {},
complete: () => {}
})
},
fail: () => {
console.log()
},
complete: () => {}
})
},
async getData() {
await uni.request({
url: 'http://localhost:8081/teacher/selectTeacherElectronicResumeInfo',
data: {
tea_id: this.tea_id
},
method: 'POST'
}).then(res => {
this.resumeInfo = res[1].data.data
})
await uni.request({
url: 'http://localhost:8081/teacher/findTeaPhoto',
data: {
tea_id: this.tea_id
},
method: 'POST'
}).then(async res => {
this.img = uni.base64ToArrayBuffer(res[1].data.data)
var save = wx.getFileSystemManager()
var rand = Math.random()
this.path = wx.env.USER_DATA_PATH + `/pic${Math.random()}.png`
console.log(this.path)
console.log('tempPath->', this.path)
await save.writeFile({
filePath: this.path,
data: this.img,
encoding: 'binary',
success: (res) => {
console.log('success')
this.drawAvatar()
},
fail: () => {
console.log('fail')
},
complete: () => {
// console.log('complete')
}
})
})
},
drawAvatar() {
this.imgUrl = this.path
console.log('imgUrl', this.imgUrl)
this.ctx.drawImage(this.imgUrl, 0.36 * this.width, 20, 100, 120)
this.ctx.draw(true)
}
},
async created() {
this.getData()
setTimeout(()=>{this.drawResume();console.log(this.resumeInfo)},500)
},
destroyed(){
wx.removeSavedFile({
filePath:this.path
})
}
}
</script>
<style lang="scss">
.line{
font-weight: bolder ;
color: grey;
margin-left: 50rpx;
}
#resume-box {
height: 150vh;
width: 100vw;
top: 0;
}
.long-text{
font-size: 24rpx;
}
.item{
font-size: 28rpx;
color:#303030;
}
.title{
margin-top:20rpx;
font-size: 40rpx;
font-weight: bold;
text-decoration:underline
}
#wxml {
position: fixed;
top: 0;
height: 150vh;
visibility: hidden;
}
#footer-btn-save {
height: 100rpx;
line-height: 100rpx;
}
.content-box-first{
margin-top: 300rpx;
margin-left: 50rpx;
width: 100vh;
}
.content-box-other{
margin-top: 50rpx;
margin-left: 50rpx;
width: 100vh;
}
</style>
然后学生版简历和教师版简历是分开实现的,用的是纯原生的canvas来做的。基本思路和教师端一致的,无非就是绘制的时候,主要选择了绝对的定位方式,对数据的行数有着极高的要求。贴一下代码,,可以看到代码行数明显多于上面。主要是后端接口返了一个很大的数据集合,需要从里面筛选,教师版的简历,是因为拿到接口以后才开始设计的,省去了很多遍历的工作。这里要注意去限制一下数据的长度和条数,不然就会和线条重叠了。
<template>
<view>
<canvas canvas-id='myResume' style="height:100vh;width:750rpx"></canvas>
<button id="button" @click="handler">导出图片</button>
</view>
</template>
<script>
export default {
data() {
return {
stu_id: 201656788,
resumeInfo: {
avatarUrl: 'http://127.0.0.1:9999/pic?pic=.png', //头像文件,这里可以给一个blob,我可以转成本地文件
//下面这三个就是字面意思
name: 'zzy',
email: '',
telephone: '15692373380',
education: {
undergraduate: { //表示本科阶段的教育经历
begin: '2018年', //开始时间
end: '2022年', //毕业时间
school: '山东大学', //学校名称
college: '软件学院', //学院名称
major: '软件工程' //专业
},
graduate: { //表示研究生阶段的教育经历
begin: '2018年',
end: '2022年',
school: '山东大学',
college: '软件学院',
major: '软件工程'
}
},
researchInterest: '复制粘贴与调参', //研究方向
findings: [ //论文或者叫成果,超过5篇取5篇文章名就行,小于等于5篇有多少给多少
'《摸鱼与划水》',
'《期末三天突击学习法》',
'《学习娱乐两边误》',
'《吃遍山东大学食堂》',
'《大学如何摸鱼三年》'
]
}
}
},
methods: {
handler() {
wx.canvasToTempFilePath({
canvasId: 'myResume',
fileType: 'jpg',
quality: 1,
success: (res) => {
wx.saveImageToPhotosAlbum({
filePath: res.tempFilePath,
success: () => {
console.log('保存成功')
},
fail: () => {},
complete: () => {}
})
},
fail: () => {},
complete: () => {}
})
}
},
async created() {
this.resumeInfo.education.undergraduate = uni.getStorageSync('undergraduateInfo')
await uni.request({
url: 'http://localhost:8081//student///selectStudentElectronicResumeInfo',
data: {
stu_id: this.stu_id
}
}).then(res => {
//科研成果部分
res = res[1].data
console.log(res)
var books = []
for (var i = 0; i < res.data.book.length; i++) {
if (res.data.book[i].monographName.length > 15) {
books.push(`《${res.data.book[i].monographName.slice(0,14)}...》`)
} else {
books.push(`《${res.data.book[i].monographName}》`)
}
}
for (var i = 0; i < res.data.paper.length; i++) {
if (res.data.paper[i].paperTitle.length > 15) {
books.push(`《${res.data.paper[i].paperTitle.slice(0,14)}...》`)
} else {
books.push(`《${res.data.paper[i].paperTitle}》`)
}
}
for (var i = 0; i < res.data.patent.length; i++) {
if (res.data.patent[i].patName.length > 15) {
books.push(`《${res.data.patent[i].patName.slice(0,14)}...》`)
} else {
books.push(`《${res.data.patent[i].patName}》`)
}
}
if (books.length < 5) {
this.resumeInfo.findings = books
} else {
this.resumeInfo.findings = books.slice(0, 5)
}
//個人信息部分
this.resumeInfo.name = res.data.stu.stuName
this.resumeInfo.email = res.data.stu.mailAddress
this.resumeInfo.telephone = res.data.stu.stuTelNum
this.resumeInfo.education.graduate.begin = res.data.stu.stuGrade + '年'
this.resumeInfo.education.graduate.end = Number(res.data.stu.stuGrade) + 3 + '' + '年'
this.resumeInfo.education.graduate.college = res.data.stu.stuDname + '学院'
this.resumeInfo.education.graduate.major = res.data.stu.stuMajor
this.resumeInfo.researchInterest = res.data.stu.stuMajor
this.resumeInfo.avatarUrl = res.data.stu.stuPhoto
console.log(this.resumeInfo.avatarUrl)
})
var width, height, img
await wx.getSystemInfo().then(res => {
width = res.windowWidth
height = res.windowHeight
})
var ctx = wx.createCanvasContext('myResume')
ctx.setFillStyle('#595959') //文字颜色:默认黑色
var linesLocation = [30, 280, 330] //控制三大部分的距离
var linesWords = ['教育经历', '研究方向', '科研成果']
// wx.downloadFile({
// url:this.resumeInfo.avatarUrl,
// success:(res)=>{
// img = res.tempFilePath
// ctx.drawImage(img,0,0,20,20)
// console.log(res.tempFilePath)
// }
// })
ctx.setFontSize(25)
ctx.fillText(this.resumeInfo.name, width * 0.32, height * 0.06)
ctx.setFontSize(15)
ctx.fillText('邮箱地址:' + this.resumeInfo.email, width * 0.32, height * 0.11)
ctx.fillText('电话号码:' + this.resumeInfo.telephone, width * 0.32, height * 0.15)
this.resumeInfo.avatarUrl = uni.base64ToArrayBuffer(this.resumeInfo.avatarUrl)
var imgSrc = this.resumeInfo.avatarUrl
var save = wx.getFileSystemManager()
var rand = Math.random()
var path = wx.env.USER_DATA_PATH + `/pic${Math.random()}.png`
console.log(path)
save.writeFile({
filePath: path,
data: this.resumeInfo.avatarUrl,
encoding: 'binary',
success: (res) => {
console.log(path)
}
})
ctx.drawImage(path, 0.06 * width, 10, 72, 90)
save.removeSavedFile({
filePath: path
})
for (var i = 0; i < linesLocation.length; i++) {
ctx.setFillStyle('#595959')
ctx.fillRect(width * 0.01, 75 + linesLocation[i], width * 0.98, 8)
ctx.fillRect(width * 0.025, 90 + linesLocation[i], width * 0.95, 2)
ctx.beginPath()
ctx.moveTo(0.04 * width, 100 + linesLocation[i])
ctx.lineTo(0.04 * width + 50, 100 + linesLocation[i])
ctx.lineTo(0.04 * width + 65, 110 + linesLocation[i])
ctx.lineTo(0.04 * width + 50, 120 + linesLocation[i])
ctx.lineTo(0.04 * width, 120 + linesLocation[i])
ctx.closePath()
ctx.fill()
ctx.arc(0.04 * width + 80, 110 + linesLocation[i], 8, 0, Math.PI * 2, true)
ctx.fill()
ctx.globalCompositeOperation = 'sourse-out'
}
for (var i = 0; i < linesWords.length; i++) {
ctx.setFillStyle('#FFFFFF')
ctx.setFontSize(12)
ctx.fillText(linesWords[i], 0.04 * width + 2, 120 + linesLocation[i] - 5)
}
ctx.setFillStyle('#595959')
ctx.setFontSize(18)
ctx.fillText('本科学历', 0.3 * width, 120 + linesLocation[0] - 5)
ctx.setFontSize(15)
ctx.fillText(
`时间:${this.resumeInfo.education.undergraduate.begin}-${this.resumeInfo.education.undergraduate.end}`,
0.3 * width,
120 + linesLocation[0] + 20
)
ctx.fillText(
`学校:${this.resumeInfo.education.undergraduate.school}`,
0.3 * width,
120 + linesLocation[0] + 40
)
ctx.fillText(
`学院:${this.resumeInfo.education.undergraduate.college}`,
0.3 * width,
120 + linesLocation[0] + 60
)
ctx.fillText(
`专业:${this.resumeInfo.education.undergraduate.major}`,
0.3 * width,
120 + linesLocation[0] + 80
)
var educationOffset = 120
ctx.setFontSize(18)
ctx.fillText('研究生学历', 0.3 * width, 120 + linesLocation[0] - 5 + educationOffset)
ctx.setFontSize(15)
ctx.fillText(
`时间:${this.resumeInfo.education.graduate.begin}-${this.resumeInfo.education.graduate.end}`,
0.3 * width,
120 + linesLocation[0] + 20 + educationOffset
)
ctx.fillText(
`学校:${this.resumeInfo.education.graduate.school}`,
0.3 * width,
120 + linesLocation[0] + 40 + educationOffset
)
ctx.fillText(
`学院:${this.resumeInfo.education.graduate.college}`,
0.3 * width,
120 + linesLocation[0] + 60 + educationOffset
)
ctx.fillText(
`专业:${this.resumeInfo.education.graduate.major}`,
0.3 * width,
120 + linesLocation[0] + 80 + educationOffset
)
ctx.setFontSize(18)
ctx.fillText(
this.resumeInfo.researchInterest,
0.3 * width,
120 + linesLocation[1]
)
ctx.setFontSize(15)
for (var i = 0; i < this.resumeInfo.findings.length; i++) {
ctx.fillText(
this.resumeInfo.findings[i],
0.3 * width,
115 + linesLocation[2] + i * 30
)
}
//调用draw()开始绘制
setTimeout(() => {
ctx.draw()
}, 3000)
}
}
</script>
<style>
#button {
color: white;
background-color: #007AFF;
height: 50rpx;
line-height: 50rpx;
width: 375rpx;
}
</style>
最后导出的图片的效果是下面这样
最后,说一下这两种实现的优劣。
依靠wxml实现的简历可以做到对内容高度的自适应,语义化比较明显,可以使用css来对文本进行控制;缺点就是不大会去画图片,因此只能依靠原生canvas去绘制头像和一些线条(当然例子里只绘制了线条)。
靠纯canvas实现就是麻烦一点,但是现在想了一下发现让页面留一个canvasContext类型的变量,并且封装一些函数来进行常用的操作,比如绘制线条和文字,理论上是可以通过计算来达到自适应的布局的。
最最最后,统计一下使用的api和插件
微信开放平台 这里主要是一些文件操作
wxml2canvas
如果是pc或者h5做电子简历会更容易,因为有dom-to-canvas这样的插件。小程序的能力相比web还是差了很多的