|
|
@@ -1,6 +1,7 @@
|
|
|
/**
|
|
|
* 名片海报生成工具
|
|
|
* 使用 Canvas 2D 绘制名片海报
|
|
|
+ * 名片底部添加名片码(二维码)
|
|
|
*/
|
|
|
|
|
|
const W = 750
|
|
|
@@ -21,13 +22,20 @@ const roundRect = (ctx, x, y, w, h, r) => {
|
|
|
ctx.closePath()
|
|
|
}
|
|
|
|
|
|
-const loadImage = (src) => {
|
|
|
+/**
|
|
|
+ * 创建图片对象 - 必须使用 canvas.createImage()
|
|
|
+ * 微信小程序 Canvas 2D 中不能用 uni.createImage()
|
|
|
+ */
|
|
|
+const createCanvasImage = (canvas, src) => {
|
|
|
return new Promise((resolve) => {
|
|
|
if (!src) return resolve(null)
|
|
|
- const img = uni.createImage()
|
|
|
- img.src = src
|
|
|
+ const img = canvas.createImage()
|
|
|
img.onload = () => resolve(img)
|
|
|
- img.onerror = () => resolve(null)
|
|
|
+ img.onerror = () => {
|
|
|
+ console.warn('图片加载失败:', src)
|
|
|
+ resolve(null)
|
|
|
+ }
|
|
|
+ img.src = src
|
|
|
})
|
|
|
}
|
|
|
|
|
|
@@ -40,6 +48,51 @@ const downloadImage = async (url) => {
|
|
|
} catch { return null }
|
|
|
}
|
|
|
|
|
|
+/**
|
|
|
+ * 将图片资源转换为Canvas可用的临时路径
|
|
|
+ */
|
|
|
+const resolveImageSrc = async (src) => {
|
|
|
+ if (!src) return null
|
|
|
+ if (src.startsWith('http')) {
|
|
|
+ try {
|
|
|
+ const res = await uni.downloadFile({ url: src })
|
|
|
+ if (res.statusCode === 200) return res.tempFilePath
|
|
|
+ return null
|
|
|
+ } catch { return null }
|
|
|
+ }
|
|
|
+ if (src.startsWith('/') || src.startsWith('.')) {
|
|
|
+ try {
|
|
|
+ const res = await uni.getImageInfo({ src })
|
|
|
+ return res.path
|
|
|
+ } catch { return null }
|
|
|
+ }
|
|
|
+ return src
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * base64 二维码转临时文件路径
|
|
|
+ */
|
|
|
+const qrBase64ToTempFile = async (base64Data) => {
|
|
|
+ if (!base64Data) return null
|
|
|
+ return new Promise((resolve) => {
|
|
|
+ try {
|
|
|
+ const pureBase64 = base64Data.replace(/^data:image\/\w+;base64,/, '')
|
|
|
+ const fileName = `${Date.now()}_qrcode.png`
|
|
|
+ const filePath = `${wx.env.USER_DATA_PATH}/${fileName}`
|
|
|
+ const fs = uni.getFileSystemManager()
|
|
|
+ fs.writeFile({
|
|
|
+ filePath,
|
|
|
+ data: pureBase64,
|
|
|
+ encoding: 'base64',
|
|
|
+ success: () => resolve(filePath),
|
|
|
+ fail: () => resolve(null)
|
|
|
+ })
|
|
|
+ } catch {
|
|
|
+ resolve(null)
|
|
|
+ }
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
const drawCircleAvatar = (ctx, img, cx, cy, r) => {
|
|
|
ctx.save()
|
|
|
ctx.beginPath()
|
|
|
@@ -69,30 +122,25 @@ const drawAvatarFallback = (ctx, cardInfo, cx, cy, r) => {
|
|
|
|
|
|
const drawContactRow = (ctx, emoji, text, x, y) => {
|
|
|
if (!text) return
|
|
|
- // 图标背景圆
|
|
|
ctx.save()
|
|
|
ctx.beginPath()
|
|
|
ctx.arc(x + 20, y + 20, 20, 0, Math.PI * 2)
|
|
|
ctx.fillStyle = 'rgba(64, 128, 255, 0.08)'
|
|
|
ctx.fill()
|
|
|
ctx.restore()
|
|
|
- // Emoji
|
|
|
ctx.font = '22px sans-serif'
|
|
|
ctx.textAlign = 'center'
|
|
|
ctx.textBaseline = 'middle'
|
|
|
ctx.fillText(emoji, x + 20, y + 20)
|
|
|
- // 文字 - 超长时自动换行
|
|
|
ctx.font = '26px sans-serif'
|
|
|
ctx.textAlign = 'left'
|
|
|
ctx.textBaseline = 'middle'
|
|
|
ctx.fillStyle = '#555555'
|
|
|
- // 计算可用宽度(图标到右边界)
|
|
|
- const textMaxWidth = 480 // 联系方式文本最大宽度
|
|
|
+ const textMaxWidth = 480
|
|
|
const textX = x + 52
|
|
|
if (ctx.measureText(text).width > textMaxWidth) {
|
|
|
- // 超长时换行显示(从垂直居中位置开始)
|
|
|
ctx.textBaseline = 'top'
|
|
|
- const lineY = y + 10 // 和图标居中对齐的偏移
|
|
|
+ const lineY = y + 10
|
|
|
fillTextWrap(ctx, text, textX, lineY, textMaxWidth, 40, 'left')
|
|
|
} else {
|
|
|
ctx.fillText(text, textX, y + 20)
|
|
|
@@ -119,7 +167,7 @@ const fillTextWrap = (ctx, text, x, y, maxWidth, lineHeight, align = 'left') =>
|
|
|
return cy + lineHeight
|
|
|
}
|
|
|
|
|
|
-export const generateCardPoster = async (cardInfo = {}) => {
|
|
|
+export const generateCardPoster = async (cardInfo = {}, qrInfo = {}) => {
|
|
|
return new Promise((resolve, reject) => {
|
|
|
const query = uni.createSelectorQuery()
|
|
|
query.select('#posterCanvas')
|
|
|
@@ -153,7 +201,7 @@ export const generateCardPoster = async (cardInfo = {}) => {
|
|
|
|
|
|
// ===== 3. 白色卡片 =====
|
|
|
const cardX = PADDING, cardY = 60
|
|
|
- const cardW = W - PADDING * 2, cardH = 660
|
|
|
+ const cardW = W - PADDING * 2, cardH = 760
|
|
|
ctx.save()
|
|
|
ctx.shadowColor = 'rgba(0,0,0,0.08)'
|
|
|
ctx.shadowBlur = 24
|
|
|
@@ -179,15 +227,20 @@ export const generateCardPoster = async (cardInfo = {}) => {
|
|
|
|
|
|
// 背景图
|
|
|
try {
|
|
|
- const cardBg = await loadImage('/static/image/home/usecard-bg.png')
|
|
|
- if (cardBg) {
|
|
|
- ctx.save()
|
|
|
- roundRect(ctx, innerX, innerY, innerW, innerH, 16)
|
|
|
- ctx.clip()
|
|
|
- ctx.drawImage(cardBg, innerX, innerY, innerW, innerH)
|
|
|
- ctx.restore()
|
|
|
+ const bgSrc = await resolveImageSrc('/static/image/home/usecard-bg.png')
|
|
|
+ if (bgSrc) {
|
|
|
+ const cardBg = await createCanvasImage(canvas, bgSrc)
|
|
|
+ if (cardBg) {
|
|
|
+ ctx.save()
|
|
|
+ roundRect(ctx, innerX, innerY, innerW, innerH, 16)
|
|
|
+ ctx.clip()
|
|
|
+ ctx.drawImage(cardBg, innerX, innerY, innerW, innerH)
|
|
|
+ ctx.restore()
|
|
|
+ }
|
|
|
}
|
|
|
- } catch (e) {}
|
|
|
+ } catch (e) {
|
|
|
+ console.warn('加载名片背景图失败:', e)
|
|
|
+ }
|
|
|
|
|
|
// ===== 5. 头像(右上角)=====
|
|
|
const avatarCx = innerX + innerW - 100
|
|
|
@@ -203,7 +256,7 @@ export const generateCardPoster = async (cardInfo = {}) => {
|
|
|
let avatarImg = null
|
|
|
if (cardInfo.avatar) {
|
|
|
const localPath = await downloadImage(cardInfo.avatar)
|
|
|
- avatarImg = await loadImage(localPath)
|
|
|
+ avatarImg = await createCanvasImage(canvas, localPath)
|
|
|
}
|
|
|
if (avatarImg) {
|
|
|
drawCircleAvatar(ctx, avatarImg, avatarCx, avatarCy, avatarR)
|
|
|
@@ -223,7 +276,6 @@ export const generateCardPoster = async (cardInfo = {}) => {
|
|
|
ctx.fillText(name, nameX, nameY)
|
|
|
const nameW = ctx.measureText(name).width
|
|
|
|
|
|
- // 职位标签
|
|
|
if (cardInfo.postName) {
|
|
|
const tagX = nameX + nameW + 14
|
|
|
ctx.font = '22px sans-serif'
|
|
|
@@ -238,27 +290,19 @@ export const generateCardPoster = async (cardInfo = {}) => {
|
|
|
ctx.fillText(cardInfo.postName, tagX + 13, nameY + 11)
|
|
|
}
|
|
|
|
|
|
- // 公司名称(最大宽度避开右上方头像)
|
|
|
+ // 公司名称
|
|
|
ctx.fillStyle = '#666666'
|
|
|
ctx.font = '26px sans-serif'
|
|
|
ctx.textAlign = 'left'
|
|
|
ctx.textBaseline = 'top'
|
|
|
- const companyMaxW = innerW - 160
|
|
|
+ const companyMaxW = innerW - 200
|
|
|
const companyName = cardInfo.companyName || '公司名称'
|
|
|
- // 如果公司名太长,手动截断
|
|
|
- ctx.font = '26px sans-serif'
|
|
|
- if (ctx.measureText(companyName).width > companyMaxW) {
|
|
|
- let display = companyName
|
|
|
- while (ctx.measureText(display + '...').width > companyMaxW && display.length > 0) {
|
|
|
- display = display.substring(0, display.length - 1)
|
|
|
- }
|
|
|
- ctx.fillText(display + '...', innerX + 44, nameY + 60)
|
|
|
- } else {
|
|
|
- ctx.fillText(companyName, innerX + 44, nameY + 60)
|
|
|
- }
|
|
|
+ const companyX = innerX + 44
|
|
|
+ const companyY = nameY + 60
|
|
|
+ const companyEndY = fillTextWrap(ctx, companyName, companyX, companyY, companyMaxW, 36, 'left')
|
|
|
|
|
|
// ===== 7. 联系方式 =====
|
|
|
- const contactY = innerY + 160
|
|
|
+ const contactY = Math.max(companyEndY + 24, innerY + 160)
|
|
|
const contactX = innerX + 44
|
|
|
const contactGap = 60
|
|
|
|
|
|
@@ -273,14 +317,40 @@ export const generateCardPoster = async (cardInfo = {}) => {
|
|
|
drawContactRow(ctx, '✉', cardInfo.email, contactX, contactY + 20 + contactGap)
|
|
|
drawContactRow(ctx, '📍', cardInfo.companyAddress, contactX, contactY + 20 + contactGap * 2)
|
|
|
|
|
|
- // ===== 8. 导出 =====
|
|
|
+ // ===== 8. 底部名片码 =====
|
|
|
+ const qrBoxSize = 130
|
|
|
+ const cardInnerBottom = cardY + cardH - 32
|
|
|
+ const qrBoxX = innerX + innerW - qrBoxSize - 44
|
|
|
+ const qrBoxY = cardInnerBottom - qrBoxSize - 44
|
|
|
+
|
|
|
+ // 灰色圆角背景框
|
|
|
+ ctx.save()
|
|
|
+ ctx.fillStyle = '#F5F7FA'
|
|
|
+ roundRect(ctx, qrBoxX - 8, qrBoxY - 8, qrBoxSize + 16, qrBoxSize + 16, 12)
|
|
|
+ ctx.fill()
|
|
|
+ ctx.restore()
|
|
|
+
|
|
|
+ // 加载二维码图片(使用 canvas.createImage())
|
|
|
+ try {
|
|
|
+ const qrLocalPath = await qrBase64ToTempFile(qrInfo.image)
|
|
|
+ if (qrLocalPath) {
|
|
|
+ const qrImg = await createCanvasImage(canvas, qrLocalPath)
|
|
|
+ if (qrImg) {
|
|
|
+ ctx.drawImage(qrImg, qrBoxX, qrBoxY, qrBoxSize, qrBoxSize)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (e) {
|
|
|
+ console.warn('加载名片码失败:', e)
|
|
|
+ }
|
|
|
+
|
|
|
+ // ===== 9. 导出 =====
|
|
|
setTimeout(() => {
|
|
|
uni.canvasToTempFilePath({
|
|
|
canvas,
|
|
|
success: (res) => resolve(res.tempFilePath),
|
|
|
fail: (err) => reject(err)
|
|
|
})
|
|
|
- }, 400)
|
|
|
+ }, 600)
|
|
|
})
|
|
|
})
|
|
|
}
|