简介:这是一份面向微信小程序开发者、企业技术团队及对电子签约场景感兴趣的进阶学习者的实战代码压缩包,聚焦在线电子合同签署组件的完整实现,帮助解决纸质合同签署效率低、身份认证与存证难等痛点。包内共10个文件,以js逻辑脚本、wxml页面结构、wxss样式表、json配置及一份md说明文档为主,整体约12KB,结构紧凑,覆盖合同展示、数字签名、存证对接与合同管理等核心模块。已有86人学习下载,适合希望快速理解小程序原生组件开发与第三方电子合同API对接思路的读者。通过阅读源码与说明,可掌握用户身份认证、签名板组件封装、合同数据加密传输及测试排错等关键实现,并借鉴其目录组织方式,将签约能力作为独立模块集成到自有小程序中,提升业务效率与用户体验。
1. 电子合同签署组件:从散落文件到可复用代码压缩包
很多做微信小程序的开发者都遇到过这种需求:业务方要在小程序里加一个“签合同”的环节,用户看完条款、手写签名、生成一份带签名的 PDF 或图片,最后双方各留一份。听起来简单,真动手时才发现坑不少——签名板在小程序里的触摸事件和 H5 完全不是一回事,canvas 导出图片的清晰度、合同模板的动态字段填充、签署完成后的文件存储与回传,每一项都能耗掉一两天。这份电子合同签署组件开发实战的代码压缩包,就是把这些环节打包成一个可复用的组件,省去从零搭轮子的时间。它适合正在做小程序端签约功能的开发者,也适合想了解小程序 canvas 签名与文件生成完整链路的同学。下面按“组件结构 → 签名板实现 → 合同生成 → 避坑 → 进阶”的顺序拆开讲。
2. 组件目录结构与核心模块拆解
拿到代码压缩包后,先别急着往项目里塞。花十分钟把目录结构看清楚,后面改起来会顺手很多。这个组件的组织方式遵循微信小程序自定义组件的标准规范,同时把签名、合同渲染、文件导出拆成了独立模块,方便按需引入。
2.1 目录树与文件职责
解压后的核心目录大致如下(不同版本可能略有差异,以实际压缩包为准):
components/ contract-sign/ index.js // 组件入口,暴露属性和方法 index.json // 组件声明 index.wxml // 签名板与合同预览的布局 index.wxss // 样式,含签名板尺寸适配 utils/ canvas-sign.js // 签名轨迹采集与重绘 contract-gen.js // 合同模板字段填充与导出 file-helper.js // 临时文件转存、base64 处理 assets/ template.png // 默认合同底图(可替换)index.js是组件的“总控”,它对外暴露了open、clear、save三个主要方法,分别对应打开签名面板、清空重签、保存签署结果。canvas-sign.js负责处理touchstart、touchmove、touchend三个事件,把触摸点转成 canvas 路径。contract-gen.js做的是把业务数据(比如甲方名称、金额、日期)填进合同模板的指定坐标位置。file-helper.js则处理小程序里比较烦人的临时文件生命周期问题。
提示:如果你只需要签名功能,可以只引入
canvas-sign.js和对应的 wxml 片段,不必整个组件搬进去。
2.2 组件属性与事件设计
组件对外暴露的属性决定了它的复用程度。这个组件设计了几个关键属性:
| 属性名 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| contractData | Object | {} | 合同动态字段,如甲方、乙方、金额 |
| templateSrc | String | '' | 合同底图路径,支持本地或临时文件 |
| penColor | String | '#000000' | 签名笔迹颜色 |
| penWidth | Number | 4 | 笔迹粗细,单位 px |
| exportFormat | String | 'png' | 导出格式,可选 png 或 jpg |
事件方面,组件在签署完成时会触发signsuccess,回传一个包含临时文件路径和签署时间戳的对象。业务层拿到这个对象后,可以上传到自己的服务器或存入云存储。这种设计的好处是组件不关心你用什么后端,只负责把文件生成出来。
常见做法是:在页面的 json 里注册组件,然后在 wxml 里写<contract-sign contractData="{{data}}" bind:signsuccess="onSignSuccess" />。onSignSuccess里拿到e.detail.tempFilePath,再走wx.uploadFile上传。整个链路清晰,没有多余的耦合。
2.3 初始化与参数配置
在页面加载时,你需要把合同数据准备好。假设合同里需要填甲方名称、乙方名称和签署日期,数据结构可以这样组织:
// pages/contract/contract.js Page({ data: { contractData: { partyA: '某公司', partyB: '某开发者', amount: '10000', date: '2025-01-01' } }, onLoad() { // 如果合同底图是网络图片,先下载到本地临时文件 wx.downloadFile({ url: 'https://example.com/template.png', // 替换为实际模板地址 success: (res) => { this.setData({ templateSrc: res.tempFilePath }); } }); }, onSignSuccess(e) { const { tempFilePath, signTime } = e.detail; // 这里做上传或本地保存 console.log('签署完成', tempFilePath, signTime); } });这段代码做了两件事:一是把合同动态字段挂到data上,组件内部会读取并渲染;二是提前把合同底图下载到本地,避免 canvas 绘制时因网络图片跨域或加载失败导致白屏。参数contractData的字段名需要和contract-gen.js里的占位符一一对应,否则填充位置会错乱。我一般会在contract-gen.js顶部维护一个坐标映射表,改字段时只动那一处。
3. 签名板实现:canvas 触摸轨迹与高清导出
签名板是整个组件里最容易翻车的部分。小程序 canvas 的 API 在基础库 2.9.0 之后分成了旧版wx.createCanvasContext和新版Canvas 2D,两者在导出图片时的表现差异很大。这个组件默认走的是 Canvas 2D 方案,因为它在高清屏上的清晰度明显更好。
3.1 触摸事件采集与轨迹绘制
签名板的核心逻辑是:监听触摸事件,把每个触摸点的坐标记录下来,然后用lineTo连成路径。关键代码如下:
// utils/canvas-sign.js class SignCanvas { constructor(canvasId, pageContext) { this.canvasId = canvasId; this.ctx = null; this.points = []; this.isDrawing = false; this.init(pageContext); } init(pageContext) { const query = pageContext.createSelectorQuery(); query.select(`#${this.canvasId}`) .fields({ node: true, size: true }) .exec((res) => { const canvas = res[0].node; const ctx = canvas.getContext('2d'); // 处理高清屏:按设备像素比放大 canvas const dpr = wx.getSystemInfoSync().pixelRatio; canvas.width = res[0].width * dpr; canvas.height = res[0].height * dpr; ctx.scale(dpr, dpr); ctx.lineWidth = 4; ctx.lineCap = 'round'; ctx.lineJoin = 'round'; ctx.strokeStyle = '#000000'; this.ctx = ctx; this.canvas = canvas; }); } touchStart(e) { this.isDrawing = true; const { x, y } = e.touches[0]; this.points = [{ x, y }]; this.ctx.beginPath(); this.ctx.moveTo(x, y); } touchMove(e) { if (!this.isDrawing) return; const { x, y } = e.touches[0]; this.points.push({ x, y }); this.ctx.lineTo(x, y); this.ctx.stroke(); } touchEnd() { this.isDrawing = false; this.ctx.closePath(); } }这里有几个参数值得注意。dpr是设备像素比,iPhone 上通常是 2 或 3,如果不做这个缩放,导出的签名图片会模糊得像马赛克。lineCap和lineJoin设为round是为了让笔迹拐角圆润,否则快速书写时会出现尖锐的折角。lineWidth设为 4 是经验值,太细了在手机上看起来费劲,太粗了签名容易糊成一团。
3.2 导出图片与清晰度控制
签名完成后,需要把 canvas 内容导出成图片。Canvas 2D 方案下用canvas.toDataURL或wx.canvasToTempFilePath:
// 导出签名图片 exportSign() { return new Promise((resolve, reject) => { wx.canvasToTempFilePath({ canvas: this.canvas, x: 0, y: 0, width: this.canvas.width, height: this.canvas.height, destWidth: this.canvas.width * 2, // 再放大一倍,提升导出清晰度 destHeight: this.canvas.height * 2, fileType: 'png', success: (res) => resolve(res.tempFilePath), fail: reject }); }); }destWidth和destHeight控制导出图片的实际像素尺寸。我一般会设成 canvas 尺寸的 2 倍,这样即使把签名图片放到合同里再整体导出,也不会因为二次缩放而模糊。fileType选png是因为它支持透明背景,方便叠加到合同底图上。如果选jpg,透明区域会变成黑色,叠上去就是一块黑斑。
3.3 签名板尺寸适配与横竖屏
小程序里 canvas 的尺寸适配是个老生常谈的问题。组件的做法是用wx.createSelectorQuery拿到 canvas 节点的实际渲染尺寸,再结合dpr设置画布缓冲区大小。这样无论手机屏幕是宽是窄,签名板的绘制区域都和视觉区域一致。
如果业务场景需要横屏签名(比如平板设备),需要在页面的onResize里重新初始化 canvas。常见做法是监听wx.onWindowResize,在回调里重新执行init方法。注意重新初始化前要先保存已有笔迹,否则用户签了一半转屏,签名就没了。这个细节在代码压缩包的canvas-sign.js里有对应处理,搜resize关键字就能找到。
4. 合同生成与文件处理:模板填充、导出与上传
签名只是第一步,把签名和合同内容合成一份完整文件才是最终交付物。这个环节涉及模板坐标映射、canvas 合成、临时文件转存三个子问题。
4.1 合同模板字段填充
合同模板通常是一张底图,上面留了空白位置给甲方、乙方、金额、日期等字段。组件的做法是在contract-gen.js里维护一个坐标表:
// utils/contract-gen.js const FIELD_POSITIONS = { partyA: { x: 120, y: 300, fontSize: 16, align: 'left' }, partyB: { x: 120, y: 360, fontSize: 16, align: 'left' }, amount: { x: 400, y: 420, fontSize: 18, align: 'right' }, date: { x: 120, y: 500, fontSize: 14, align: 'left' } }; function fillContract(ctx, data, templateImg) { // 先绘制底图 ctx.drawImage(templateImg, 0, 0, canvasWidth, canvasHeight); // 再逐字段绘制文字 Object.keys(FIELD_POSITIONS).forEach(key => { const pos = FIELD_POSITIONS[key]; ctx.font = `${pos.fontSize}px sans-serif`; ctx.fillStyle = '#333333'; ctx.textAlign = pos.align; ctx.fillText(data[key] || '', pos.x, pos.y); }); }坐标表里的x、y是相对于合同底图左上角的像素位置。这个值需要根据实际模板调整,没有万能公式。我一般会先用画图工具在模板上量一下大致位置,填进去后再微调两轮。fontSize和align也是按模板留白区域来定,align设为right时x坐标对应的是文字右边缘。
4.2 合成签名与合同并导出
签名图片和合同底图合成时,需要把签名绘制到合同指定的签署区域。代码逻辑如下:
async function composeContract(signTempPath, contractData, templatePath) { const canvas = wx.createOffscreenCanvas({ type: '2d', width: 800, height: 1200 }); const ctx = canvas.getContext('2d'); // 加载底图 const templateImg = canvas.createImage(); await new Promise((resolve) => { templateImg.onload = resolve; templateImg.src = templatePath; }); ctx.drawImage(templateImg, 0, 0, 800, 1200); // 填充文字字段 fillContract(ctx, contractData, templateImg); // 加载签名图片并绘制到签署区 const signImg = canvas.createImage(); await new Promise((resolve) => { signImg.onload = resolve; signImg.src = signTempPath; }); ctx.drawImage(signImg, 500, 800, 200, 100); // 签署区坐标和尺寸 // 导出最终合同 return new Promise((resolve) => { wx.canvasToTempFilePath({ canvas, success: (res) => resolve(res.tempFilePath) }); }); }这里用了wx.createOffscreenCanvas创建离屏画布,好处是不占用页面渲染资源,适合在后台做图片合成。签署区的drawImage参数(500, 800, 200, 100)分别对应 x 坐标、y 坐标、宽度、高度,需要根据合同模板上签名栏的实际位置来改。如果签名被拉伸变形,检查一下宽高比是否和签名原图一致。
4.3 临时文件转存与上传
小程序里的临时文件在本次启动后有效,重启后可能被清理。所以签署完成后要尽快转存或上传。组件提供了file-helper.js来做这件事:
// utils/file-helper.js function saveToLocal(tempFilePath) { return new Promise((resolve, reject) => { wx.saveFile({ tempFilePath, success: (res) => resolve(res.savedFilePath), fail: reject }); }); } function uploadToServer(tempFilePath, url) { return new Promise((resolve, reject) => { wx.uploadFile({ url, filePath: tempFilePath, name: 'contract', success: (res) => resolve(JSON.parse(res.data)), fail: reject }); }); }wx.saveFile把临时文件转成持久化文件,但注意小程序对本地存储有 10MB 的总量限制,合同文件多了会存不下。所以更稳妥的做法是直接上传到服务器,本地只保留最近几份。wx.uploadFile的name参数要和后端接收字段一致,否则后端拿不到文件。上传超时时间默认是 60 秒,合同文件大的话可以在wx.uploadFile里加timeout参数。
5. 避坑与常见问题排查
这一章记录的是我在实际项目里踩过的坑,每一条都对应一个具体的现象和解决方式。如果你在调试时遇到类似情况,可以直接对照排查。
5.1 签名笔迹断断续续或偏移
现象:手指快速滑动时,签名轨迹出现断点,或者笔迹位置和手指位置有明显偏移。
原因:touchmove事件的触发频率有限,快速滑动时两个事件之间的坐标跨度大,lineTo直接连线会跳过中间点。偏移问题通常是因为 canvas 的 CSS 尺寸和缓冲区尺寸不一致,触摸坐标没有按比例映射。
解决:在touchmove里用二次贝塞尔曲线平滑连接,或者至少把lineTo改成quadraticCurveTo。偏移问题需要检查canvas.width和canvas.style.width的比例,触摸坐标要乘以dpr再使用。组件里canvas-sign.js的touchMove方法已经做了坐标转换,如果你自己改过样式,记得同步改转换逻辑。
5.2 导出图片模糊或背景变黑
现象:导出的签名图片放大后模糊,或者背景从透明变成黑色。
原因:模糊是因为 canvas 缓冲区尺寸等于 CSS 尺寸,没有乘以dpr。背景变黑是因为导出格式选了jpg,而 jpg 不支持透明通道,透明区域默认填充黑色。
解决:初始化 canvas 时把canvas.width设为cssWidth * dpr,并调用ctx.scale(dpr, dpr)。导出格式强制用png。如果业务必须用 jpg,在绘制前先给 canvas 填充白色背景。
5.3 合同底图加载失败导致白屏
现象:签名完成后合成合同,导出的图片只有签名没有合同底图,或者整个画布是白的。
原因:canvas.createImage加载网络图片时,如果图片没有下载到本地,或者域名没有加入小程序的 downloadFile 合法域名列表,加载会静默失败。
解决:提前用wx.downloadFile把模板图片下载到本地临时路径,再用本地路径去createImage。同时检查小程序后台的 downloadFile 合法域名配置,把模板图片所在的域名加进去。如果模板是本地资源,直接放assets目录引用即可。
5.4 临时文件在真机上找不到
现象:开发者工具里一切正常,真机上签署完成后上传失败,提示文件不存在。
原因:开发者工具对临时文件的保留策略比真机宽松,真机上临时文件可能在页面跳转或一段时间后被清理。
解决:签署完成后立即调用wx.saveFile或直接上传,不要等用户点了别的按钮再处理。如果业务允许,可以在onHide里也做一次转存。另外注意wx.saveFile的存储上限,定期清理旧文件。
5.5 签名区域在全面屏手机上被遮挡
现象:iPhone 全面屏机型上,签名板底部被 Home Indicator 挡住,用户签不到最下面。
原因:页面布局没有考虑安全区域,canvas 的底部和屏幕底部重合。
解决:在 wxss 里给签名板容器加padding-bottom: env(safe-area-inset-bottom),或者用wx.getSystemInfoSync().safeArea拿到安全区域高度,动态设置 canvas 高度。这个适配在组件的index.wxss里有注释说明,搜safe-area就能找到。
6. 进阶技巧:签名压感模拟与合同验签思路
基础功能跑通之后,如果想让签名体验更接近真实纸笔,可以试试压感模拟。小程序目前没有直接暴露触摸压力值,但可以通过触摸面积来近似——e.touches[0]里虽然没有force字段,但部分安卓机型支持e.touches[0].radius或e.touches[0].force。我一般会先判断字段是否存在,存在就根据压力值动态调整lineWidth:
touchMove(e) { if (!this.isDrawing) return; const touch = e.touches[0]; const { x, y } = touch; // 压感模拟:有 force 字段时按压力调整笔宽 const force = touch.force || 0.5; const dynamicWidth = 2 + force * 6; // 笔宽范围 2~8 this.ctx.lineWidth = dynamicWidth; this.ctx.lineTo(x, y); this.ctx.stroke(); this.points.push({ x, y, force }); }这段代码在支持压感的设备上会让签名有明显的粗细变化,写起来更像真实笔迹。不支持压感的设备会走默认值 0.5,笔宽稳定在 5 左右,也不会难看。注意每次touchmove都改lineWidth会影响性能,可以每 3 个点更新一次。
另一个值得花时间的方向是合同验签。虽然小程序端做完整验签不现实,但可以在生成合同时把关键字段(甲方、乙方、金额、签署时间)拼成一个字符串,用wx.getFileSystemManager().writeFile写一份校验文件一起上传。后端收到后重新拼一遍字符串做比对,能挡住大部分篡改场景。这个思路在代码压缩包的contract-gen.js里留了扩展接口,搜verify关键字能看到预留的函数签名。
还有一个实际项目里总结的习惯:每次改完合同模板坐标,我都会先用一组假数据跑一遍完整流程,把导出的合同图片存到手机相册,放大检查每个字段的位置和签名清晰度。这个动作花不了两分钟,但能避免上线后用户反馈“金额印到边框外面了”这种尴尬。希望帮到你。
本文还有配套的精品资源,点击获取