【项目难点】使用canvas完成电子简历功能

16 篇文章 0 订阅
7 篇文章 0 订阅

要求能够实现将个人照片、信息,展示到小程序页面中,并可以导出。但是因为后端没有一个单独的图片服务器,并且图片是以一个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还是差了很多的

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
当然可以!以下是一个使用Canvas实现电子签名功能的示例代码。 HTML部分: ```html < id="signature-canvas" width="400 height="200"></canvas> <button onclick="clearCanvas()">清除</button> <button onclick="saveSignature()">保存</button> ``` JavaScript部分: ```javascript var canvas = document.getElementById('signature-canvas'); var ctx = canvas.getContext('2d'); var isDrawing = false; var lastX = 0; var lastY = 0; canvas.addEventListener('mousedown', startDrawing); canvas.addEventListener('mousemove', draw); canvas.addEventListener('mouseup', stopDrawing); canvas.addEventListener('mouseout', stopDrawing); function startDrawing(e) { isDrawing = true; [lastX, lastY] = [e.offsetX, e.offsetY]; } function draw(e) { if (!isDrawing) return; ctx.beginPath(); ctx.moveTo(lastX, lastY); ctx.lineTo(e.offsetX, e.offsetY); ctx.stroke(); [lastX, lastY] = [e.offsetX, e.offsetY]; } function stopDrawing() { isDrawing = false; } function clearCanvas() { ctx.clearRect(0, 0, canvas.width, canvas.height); } function saveSignature() { var dataURL = canvas.toDataURL(); console.log(dataURL); // 这里可以将dataURL发送到后端进行保存 } ``` 这段代码创建了一个带有布和两个按钮的HTML页面。当用户在布上按下鼠标并移动时,会在布上绘制路径。用户可以使用“清除”按钮来清除布上的签名,使用“保存”按钮可以获取签名的base64编码数据URL,你可以将这个数据URL发送到后端进行保存。 希望这可以帮助到你!如有任何问题,请随时提问。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值