|
|
@@ -27,8 +27,9 @@ const roundRect = (ctx, x, y, w, h, r) => {
|
|
|
* 微信小程序 Canvas 2D 中不能用 uni.createImage()
|
|
|
*/
|
|
|
const createCanvasImage = (canvas, src) => {
|
|
|
+
|
|
|
return new Promise((resolve) => {
|
|
|
- if (!src) return resolve(null)
|
|
|
+ if (!src || !canvas) return resolve(null)
|
|
|
const img = canvas.createImage()
|
|
|
img.onload = () => resolve(img)
|
|
|
img.onerror = () => {
|
|
|
@@ -120,30 +121,27 @@ const drawAvatarFallback = (ctx, cardInfo, cx, cy, r) => {
|
|
|
ctx.restore()
|
|
|
}
|
|
|
|
|
|
-const drawContactRow = (ctx, emoji, text, x, y) => {
|
|
|
+const drawContactRow = (ctx, iconImg, 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()
|
|
|
- ctx.font = '22px sans-serif'
|
|
|
- ctx.textAlign = 'center'
|
|
|
- ctx.textBaseline = 'middle'
|
|
|
- ctx.fillText(emoji, x + 20, y + 20)
|
|
|
+ // 图标(40x40)
|
|
|
+ const iconSize = 32
|
|
|
+ const iconX = x
|
|
|
+ const iconY = y
|
|
|
+ if (iconImg) {
|
|
|
+ ctx.drawImage(iconImg, iconX, iconY, iconSize, iconSize)
|
|
|
+ }
|
|
|
ctx.font = '26px sans-serif'
|
|
|
ctx.textAlign = 'left'
|
|
|
ctx.textBaseline = 'middle'
|
|
|
- ctx.fillStyle = '#555555'
|
|
|
+ ctx.fillStyle = '#202020'
|
|
|
const textMaxWidth = 480
|
|
|
- const textX = x + 52
|
|
|
+ const textX = x + iconSize + 12
|
|
|
if (ctx.measureText(text).width > textMaxWidth) {
|
|
|
ctx.textBaseline = 'top'
|
|
|
- const lineY = y + 10
|
|
|
+ const lineY = y + 4
|
|
|
fillTextWrap(ctx, text, textX, lineY, textMaxWidth, 40, 'left')
|
|
|
} else {
|
|
|
- ctx.fillText(text, textX, y + 20)
|
|
|
+ ctx.fillText(text, textX, y + iconSize / 2)
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -187,9 +185,9 @@ export const generateCardPoster = async (cardInfo = {}, qrInfo = {}) => {
|
|
|
|
|
|
// ===== 1. 渐变背景 =====
|
|
|
const bgGrad = ctx.createLinearGradient(0, 0, 0, H)
|
|
|
- bgGrad.addColorStop(0, '#4A90E2')
|
|
|
- bgGrad.addColorStop(0.5, '#6FB3F2')
|
|
|
- bgGrad.addColorStop(1, '#B0E0E6')
|
|
|
+ bgGrad.addColorStop(0, '#155DFC')
|
|
|
+ bgGrad.addColorStop(0.5, '#155DFC')
|
|
|
+ bgGrad.addColorStop(1, '#155DFC')
|
|
|
ctx.fillStyle = bgGrad
|
|
|
ctx.fillRect(0, 0, W, H)
|
|
|
|
|
|
@@ -199,44 +197,24 @@ export const generateCardPoster = async (cardInfo = {}, qrInfo = {}) => {
|
|
|
ctx.beginPath(); ctx.arc(680, 320, 60, 0, Math.PI * 2); ctx.fill()
|
|
|
ctx.beginPath(); ctx.arc(-20, 640, 80, 0, Math.PI * 2); ctx.fill()
|
|
|
|
|
|
- // ===== 3. 白色卡片 =====
|
|
|
- const cardX = PADDING, cardY = 60
|
|
|
- const cardW = W - PADDING * 2, cardH = 480
|
|
|
- ctx.save()
|
|
|
- ctx.shadowColor = 'rgba(0,0,0,0.08)'
|
|
|
- ctx.shadowBlur = 24
|
|
|
- ctx.shadowOffsetY = 6
|
|
|
- ctx.fillStyle = '#FFFFFF'
|
|
|
- roundRect(ctx, cardX, cardY, cardW, cardH, 24)
|
|
|
- ctx.fill()
|
|
|
- ctx.restore()
|
|
|
-
|
|
|
- // ===== 4. 内部名片卡片 =====
|
|
|
- const innerX = cardX + 32
|
|
|
- const innerY = cardY + 32
|
|
|
- const innerW = cardW - 64
|
|
|
- const innerH = cardH - 64
|
|
|
- ctx.save()
|
|
|
- ctx.shadowColor = 'rgba(0,0,0,0.05)'
|
|
|
- ctx.shadowBlur = 16
|
|
|
- ctx.shadowOffsetY = 3
|
|
|
- ctx.fillStyle = '#FFFFFF'
|
|
|
- roundRect(ctx, innerX, innerY, innerW, innerH, 16)
|
|
|
- ctx.fill()
|
|
|
- ctx.restore()
|
|
|
+ // ===== 3. 白色卡片(去掉白色背景,只保留圆角裁剪区域)=====
|
|
|
+ const cardX = 20, cardY = 60
|
|
|
+ const cardW = W - 40, cardH = 480
|
|
|
+ const innerPad = 16
|
|
|
+ const innerX = cardX + innerPad
|
|
|
+ const innerY = cardY + innerPad
|
|
|
+ const innerW = cardW - innerPad * 2
|
|
|
+ const innerH = cardH - innerPad * 2
|
|
|
|
|
|
- // 背景图
|
|
|
+ // 背景图(静态资源直接传路径,canvas.createImage 支持加载本地包内资源)
|
|
|
try {
|
|
|
- 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()
|
|
|
- }
|
|
|
+ const cardBg = await createCanvasImage(canvas, '/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()
|
|
|
}
|
|
|
} catch (e) {
|
|
|
console.warn('加载名片背景图失败:', e)
|
|
|
@@ -244,7 +222,7 @@ export const generateCardPoster = async (cardInfo = {}, qrInfo = {}) => {
|
|
|
|
|
|
// ===== 5. 头像(右上角)=====
|
|
|
const avatarCx = innerX + innerW - 100
|
|
|
- const avatarCy = innerY + 70
|
|
|
+ const avatarCy = innerY + 115
|
|
|
const avatarR = 60
|
|
|
ctx.save()
|
|
|
ctx.beginPath()
|
|
|
@@ -266,7 +244,7 @@ export const generateCardPoster = async (cardInfo = {}, qrInfo = {}) => {
|
|
|
|
|
|
// ===== 6. 姓名 + 职位 + 公司 =====
|
|
|
const nameX = innerX + 44
|
|
|
- const nameY = innerY + 50
|
|
|
+ const nameY = innerY + 70
|
|
|
|
|
|
ctx.fillStyle = '#202020'
|
|
|
ctx.font = 'bold 44px sans-serif'
|
|
|
@@ -277,21 +255,15 @@ export const generateCardPoster = async (cardInfo = {}, qrInfo = {}) => {
|
|
|
const nameW = ctx.measureText(name).width
|
|
|
|
|
|
if (cardInfo.postName) {
|
|
|
- const tagX = nameX + nameW + 14
|
|
|
ctx.font = '22px sans-serif'
|
|
|
- const tagTextW = ctx.measureText(cardInfo.postName).width
|
|
|
- const tagW = tagTextW + 26
|
|
|
- ctx.fillStyle = 'rgba(68, 110, 255, 0.10)'
|
|
|
- roundRect(ctx, tagX, nameY + 6, tagW, 34, 8)
|
|
|
- ctx.fill()
|
|
|
- ctx.fillStyle = '#446EFF'
|
|
|
+ ctx.fillStyle = '#202020'
|
|
|
ctx.textAlign = 'left'
|
|
|
ctx.textBaseline = 'top'
|
|
|
- ctx.fillText(cardInfo.postName, tagX + 13, nameY + 11)
|
|
|
+ ctx.fillText(cardInfo.postName, nameX + nameW + 14, nameY + 11)
|
|
|
}
|
|
|
|
|
|
// 公司名称
|
|
|
- ctx.fillStyle = '#666666'
|
|
|
+ ctx.fillStyle = '#202020'
|
|
|
ctx.font = '26px sans-serif'
|
|
|
ctx.textAlign = 'left'
|
|
|
ctx.textBaseline = 'top'
|
|
|
@@ -306,16 +278,14 @@ export const generateCardPoster = async (cardInfo = {}, qrInfo = {}) => {
|
|
|
const contactX = innerX + 44
|
|
|
const contactGap = 60
|
|
|
|
|
|
- ctx.strokeStyle = '#EEEEEE'
|
|
|
- ctx.lineWidth = 1
|
|
|
- ctx.beginPath()
|
|
|
- ctx.moveTo(contactX, contactY)
|
|
|
- ctx.lineTo(innerX + innerW - 44, contactY)
|
|
|
- ctx.stroke()
|
|
|
+ // 预加载联系方式图标
|
|
|
+ const phoneIcon = await createCanvasImage(canvas, '/static/image/public/phone-icon.png')
|
|
|
+ const emailIcon = await createCanvasImage(canvas, '/static/image/public/email-icon.png')
|
|
|
+ const addressIcon = await createCanvasImage(canvas, '/static/image/public/address-icon.png')
|
|
|
|
|
|
- drawContactRow(ctx, '📞', cardInfo.phonenumber, contactX, contactY + 20)
|
|
|
- drawContactRow(ctx, '✉', cardInfo.email, contactX, contactY + 20 + contactGap)
|
|
|
- drawContactRow(ctx, '📍', cardInfo.companyAddress, contactX, contactY + 20 + contactGap * 2)
|
|
|
+ drawContactRow(ctx, phoneIcon, cardInfo.phonenumber, contactX, contactY + 20)
|
|
|
+ drawContactRow(ctx, emailIcon, cardInfo.email, contactX, contactY + 20 + contactGap)
|
|
|
+ drawContactRow(ctx, addressIcon, cardInfo.companyAddress, contactX, contactY + 20 + contactGap * 2)
|
|
|
|
|
|
// ===== 8. 导出 =====
|
|
|
setTimeout(() => {
|