| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314 |
- /**
- * 名片海报生成工具
- * 使用 Canvas 手动绘制名片
- */
- /**
- * 生成名片海报
- * @param {Object} options - 配置选项
- * @param {Object} options.cardInfo - 名片信息
- * @returns {Promise<String>} - 生成的图片临时路径
- */
- 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 + 10)
- // 绘制文字
- 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
- }
|