/** * 名片海报生成工具 * 使用 Canvas 手动绘制名片 */ /** * 生成名片海报 * @param {Object} options - 配置选项 * @param {Object} options.cardInfo - 名片信息 * @returns {Promise} - 生成的图片临时路径 */ export const generateCardPoster = async (cardInfo) => { return new Promise((resolve, reject) => { try { // 创建离屏 canvas const query = uni.createSelectorQuery() query.select('#posterCanvas') .fields({ node: true, size: true }) .exec(async (res) => { if (!res[0]) { reject(new Error('Canvas 节点未找到')) return } const canvas = res[0].node const ctx = canvas.getContext('2d') // 设置 canvas 尺寸(高分辨率) const width = 750 const height = 800 const dpr = uni.getSystemInfoSync().pixelRatio canvas.width = width * dpr canvas.height = height * dpr ctx.scale(dpr, dpr) // 1. 绘制背景渐变 const gradient = ctx.createLinearGradient(0, 0, 0, height) gradient.addColorStop(0, '#4A90E2') gradient.addColorStop(0.5, '#6FB3F2') gradient.addColorStop(1, '#87CEEB') ctx.fillStyle = gradient ctx.fillRect(0, 0, width, height) // 2. 绘制白色卡片背景 ctx.fillStyle = '#ffffff' drawRoundRect(ctx, 40, 40, 670, 720, 24) ctx.fill() // 3. 绘制头像 const avatarPath = cardInfo.avatar || '/static/image/public/avatar-default.png' await drawAvatar(ctx, avatarPath, 375, 160, 160) // 4. 绘制姓名和职位 ctx.fillStyle = '#202020' ctx.font = 'bold 44px sans-serif' ctx.textAlign = 'center' ctx.fillText(cardInfo.nickName || '用户', width / 2, 380) // 职位标签背景 ctx.fillStyle = 'rgba(68, 110, 255, 0.1)' drawRoundRect(ctx, (width - 180) / 2, 400, 180, 44, 8) ctx.fill() ctx.fillStyle = '#446eff' ctx.font = '26px sans-serif' ctx.textAlign = 'center' ctx.fillText(cardInfo.postName || '职位', width / 2, 430) // 5. 绘制公司名称 ctx.fillStyle = '#666666' ctx.font = '30px sans-serif' ctx.textAlign = 'center' wrapText(ctx, cardInfo.companyName || '公司名称', width / 2, 480, 500, 36) // 6. 绘制联系方式 const contactY = 560 const contactGap = 65 ctx.textAlign = 'left' // 电话 if (cardInfo.phonenumber) { await drawIconText(ctx, 'phone', cardInfo.phonenumber, width / 2 - 130, contactY) } // 邮箱 if (cardInfo.email) { await drawIconText(ctx, 'email', cardInfo.email, width / 2 - 130, contactY + contactGap) } // 地址 if (cardInfo.companyAddress) { await drawIconText(ctx, 'location', cardInfo.companyAddress, width / 2 - 130, contactY + contactGap * 2, ) } // 导出图片 setTimeout(() => { uni.canvasToTempFilePath({ canvas: canvas, success: (res) => { console.log('海报生成成功:', res.tempFilePath) resolve(res.tempFilePath) }, fail: (err) => { console.error('海报导出失败:', err) reject(err) } }) }, 300) }) } catch (error) { console.error('海报生成失败:', error) reject(error) } }) } /** * 绘制圆角矩形(兼容微信小程序) */ const drawRoundRect = (ctx, x, y, width, height, radius) => { ctx.beginPath() ctx.moveTo(x + radius, y) ctx.lineTo(x + width - radius, y) ctx.quadraticCurveTo(x + width, y, x + width, y + radius) ctx.lineTo(x + width, y + height - radius) ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height) ctx.lineTo(x + radius, y + height) ctx.quadraticCurveTo(x, y + height, x, y + height - radius) ctx.lineTo(x, y + radius) ctx.quadraticCurveTo(x, y, x + radius, y) ctx.closePath() } /** * 绘制头像 */ const drawAvatar = async (ctx, avatarUrl, x, y, size) => { return new Promise((resolve) => { if (avatarUrl.startsWith('http')) { // 网络图片 downloadAndDraw(ctx, avatarUrl, x, y, size, resolve) } else { drawCircularImage(ctx, avatarUrl, x, y, size, resolve) } }) } /** * 下载并绘制网络图片 */ const downloadAndDraw = async (ctx, url, x, y, size, resolve) => { try { const res = await uni.downloadFile({ url: url }) if (res.statusCode === 200) { drawCircularImage(ctx, res.tempFilePath, x, y, size, resolve) } else { console.error('图片下载失败:', res.statusCode) resolve() } } catch (error) { console.error('下载图片异常:', error) resolve() } } /** * 绘制圆形图片 */ const drawCircularImage = (ctx, imageUrl, x, y, size, callback) => { const image = uni.createImage() image.src = imageUrl image.onload = () => { ctx.save() ctx.beginPath() ctx.arc(x, y, size / 2, 0, 2 * Math.PI) ctx.clip() ctx.drawImage(image, x - size / 2, y - size / 2, size, size) ctx.restore() if (callback) callback() } image.onerror = (err) => { console.error('图片加载失败:', err) if (callback) callback() } } /** * 绘制图标和文字 */ const drawIconText = async (ctx, type, text, x, y, maxWidth = 350) => { return new Promise((resolve) => { const iconSize = 32 const iconY = y - iconSize / 2 // 图标颜色 const iconColors = { phone: '#4080FF', email: '#4080FF', location: '#4080FF' } ctx.fillStyle = iconColors[type] || '#4080FF' // 绘制图标 const icons = { phone: '', email: '', location: '' } ctx.font = `${iconSize}px sans-serif` ctx.textAlign = 'center' ctx.fillText(icons[type] || '●', x - maxWidth / 2 + 30, iconY + iconSize / 2 + 5) // 绘制文字 ctx.fillStyle = '#666666' ctx.font = '26px sans-serif' ctx.textAlign = 'left' // 文字截断处理 let displayText = text if (text.length > 35) { displayText = text.substring(0, 33) + '...' } ctx.fillText(displayText, x - maxWidth / 2 + 70, y + 8) setTimeout(resolve, 50) }) } /** * 绘制换行文字 */ const wrapText = (ctx, text, x, y, maxWidth, lineHeight) => { if (!text) return const words = text.split('') let line = '' let currentY = y for (let i = 0; i < words.length; i++) { const testLine = line + words[i] const metrics = ctx.measureText(testLine) if (metrics.width > maxWidth && i > 0) { ctx.fillText(line, x, currentY) line = words[i] currentY += lineHeight } else { line = testLine } } ctx.fillText(line, x, currentY) } /** * 保存海报到相册 */ export const savePosterToAlbum = async (tempFilePath) => { return new Promise((resolve, reject) => { uni.authorize({ scope: 'scope.writePhotosAlbum', success: async () => { uni.saveImageToPhotosAlbum({ filePath: tempFilePath, success: () => resolve(), fail: (err) => reject(err) }) }, fail: (err) => { uni.showModal({ title: '提示', content: '需要授权才能保存到相册', success: (res) => { if (res.confirm) { uni.openSetting({ success: (settingRes) => { if (settingRes.authSetting[ 'scope.writePhotosAlbum' ]) { savePosterToAlbum( tempFilePath).then( resolve).catch( reject) } else { reject(new Error('用户拒绝授权')) } }, fail: (err) => reject(err) }) } else { reject(new Error('用户取消授权')) } } }) } }) }) } export default { generateCardPoster, savePosterToAlbum }