fix: 修复报告分享页

This commit is contained in:
MaDaLei 2026-04-17 12:50:44 +08:00
parent 3be6d0bf9b
commit f855675f09
2 changed files with 129 additions and 171 deletions

View File

@ -307,11 +307,10 @@ const afterMorePhotos = computed(() => afterPhotos.value.slice(1))
const shareTitle = computed(() => { const shareTitle = computed(() => {
const r = reportData.value const r = reportData.value
if (!r) return '宠物洗护美容报告' if (!r) return '洗护前后对比'
const store = r.store?.name || '宠伴生活馆' const store = r.store?.name || '宠伴生活馆'
const pet = r.petName || '爱宠' const pet = r.petName || '爱宠'
const svc = r.serviceType || '洗护美容' return `${pet}的洗护蜕变|${store}`
return `${store}${pet}${svc}报告`
}) })
const shareCoverUrl = computed(() => { const shareCoverUrl = computed(() => {

View File

@ -1,10 +1,11 @@
/** /**
* 洗美报告分享海报 简约风格 v2 * 洗美报告分享海报 朋友圈向大图对比为主弱化服务信息
* H5 / 小程序 canvas 2d 共用 * H5 / 小程序 canvas 2d 共用
*/ */
export const POSTER_W = 750 export const POSTER_W = 750
export const POSTER_H = 1200 /** 竖版比例偏手机屏,利于发朋友圈 */
export const POSTER_H = 1180
const FONT = '"PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif' const FONT = '"PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif'
@ -25,29 +26,13 @@ function fillRR(ctx, x, y, w, h, r, fill) {
ctx.fill() ctx.fill()
} }
function wrapText(ctx, text, x, y, maxW, lineH, maxLines) { function drawBgGradient(ctx) {
if (!text) return y const g = ctx.createLinearGradient(0, 0, 0, POSTER_H)
const chars = String(text).split('') g.addColorStop(0, '#fffefb')
let line = '' g.addColorStop(0.42, '#f4faf6')
let cy = y g.addColorStop(1, '#e5f4ec')
let n = 0 ctx.fillStyle = g
for (let i = 0; i < chars.length; i++) { ctx.fillRect(0, 0, POSTER_W, POSTER_H)
const test = line + chars[i]
if (ctx.measureText(test).width > maxW && line) {
n++
if (maxLines && n >= maxLines) {
ctx.fillText(line + '…', x, cy)
return cy + lineH
}
ctx.fillText(line, x, cy)
line = chars[i]
cy += lineH
} else {
line = test
}
}
if (line) { ctx.fillText(line, x, cy); cy += lineH }
return cy
} }
/** /**
@ -55,201 +40,175 @@ function wrapText(ctx, text, x, y, maxW, lineH, maxLines) {
*/ */
export function drawReportPoster(ctx, data) { export function drawReportPoster(ctx, data) {
const { const {
storeName, storePhone, storeAddr, storeName,
petName, serviceType, timeStr, staffName, petName,
remark, beforeImg, afterImg, qrImg, beforeImg,
beforeCount, afterCount afterImg,
qrImg
} = data } = data
const name = storeName || '宠伴生活馆' const name = storeName || '宠伴生活馆'
const PAD = 40 const PAD = 28
const pet = petName || '宝贝'
// ── 背景 ── drawBgGradient(ctx)
ctx.fillStyle = '#fafaf8'
ctx.fillRect(0, 0, POSTER_W, POSTER_H)
// ── 顶部品牌条 ──
const barH = 100
ctx.fillStyle = '#1a1a1a'
ctx.fillRect(0, 0, POSTER_W, barH)
// ── 顶部:轻量品牌(不抢镜)──
ctx.textAlign = 'center' ctx.textAlign = 'center'
ctx.fillStyle = '#ffffff' ctx.fillStyle = 'rgba(22, 101, 52, 0.88)'
ctx.font = `bold 30px ${FONT}` ctx.font = `600 22px ${FONT}`
ctx.fillText(name, POSTER_W / 2, 60) ctx.fillText(name, POSTER_W / 2, 48)
// ── 照片对比区 ── ctx.fillStyle = 'rgba(100, 116, 139, 0.85)'
const photoY = barH ctx.font = `13px ${FONT}`
const photoH = 360 ctx.fillText('洗护美容 · 真实对比', POSTER_W / 2, 76)
const halfW = POSTER_W / 2
// ── 主视觉:大尺寸双图 + 相框感 ──
const frameX = PAD
const frameY = 98
const frameW = POSTER_W - PAD * 2
const frameR = 28
const innerPad = 10
const photoY = frameY + innerPad
const photoH = 560
const gap = 6
const innerW = frameW - innerPad * 2
const halfW = (innerW - gap) / 2
// 外框阴影(模拟卡片)
ctx.save()
ctx.shadowColor = 'rgba(15, 23, 42, 0.08)'
ctx.shadowBlur = 24
ctx.shadowOffsetY = 8
fillRR(ctx, frameX, frameY, frameW, innerPad * 2 + photoH, frameR, '#ffffff')
ctx.restore()
ctx.save()
roundRect(ctx, frameX + innerPad, photoY, innerW, photoH, 20)
ctx.clip()
if (beforeImg) { if (beforeImg) {
ctx.save()
ctx.beginPath()
ctx.rect(0, photoY, halfW - 1.5, photoH)
ctx.clip()
const bw = beforeImg.width || halfW const bw = beforeImg.width || halfW
const bh = beforeImg.height || photoH const bh = beforeImg.height || photoH
const scale = Math.max(halfW / bw, photoH / bh) const scale = Math.max(halfW / bw, photoH / bh)
const dw = bw * scale const dw = bw * scale
const dh = bh * scale const dh = bh * scale
ctx.drawImage(beforeImg, (halfW - dw) / 2, photoY + (photoH - dh) / 2, dw, dh) ctx.drawImage(beforeImg, frameX + innerPad + (halfW - dw) / 2, photoY + (photoH - dh) / 2, dw, dh)
ctx.restore()
} else { } else {
ctx.fillStyle = '#f0f0ee' ctx.fillStyle = '#eef2f0'
ctx.fillRect(0, photoY, halfW - 1.5, photoH) ctx.fillRect(frameX + innerPad, photoY, halfW, photoH)
ctx.fillStyle = '#bbbbb5' ctx.fillStyle = '#94a3b8'
ctx.font = `14px ${FONT}` ctx.font = `14px ${FONT}`
ctx.textAlign = 'center' ctx.textAlign = 'center'
ctx.fillText('暂无照片', halfW / 2, photoY + photoH / 2 + 5) ctx.fillText('洗前', frameX + innerPad + halfW / 2, photoY + photoH / 2)
} }
if (afterImg) { if (afterImg) {
ctx.save() const ax = frameX + innerPad + halfW + gap
ctx.beginPath()
ctx.rect(halfW + 1.5, photoY, halfW - 1.5, photoH)
ctx.clip()
const aw = afterImg.width || halfW const aw = afterImg.width || halfW
const ah = afterImg.height || photoH const ah = afterImg.height || photoH
const scale = Math.max(halfW / aw, photoH / ah) const scale = Math.max(halfW / aw, photoH / ah)
const dw = aw * scale const dw = aw * scale
const dh = ah * scale const dh = ah * scale
ctx.drawImage(afterImg, halfW + 1.5 + (halfW - dw) / 2, photoY + (photoH - dh) / 2, dw, dh) ctx.drawImage(afterImg, ax + (halfW - dw) / 2, photoY + (photoH - dh) / 2, dw, dh)
ctx.restore()
} else { } else {
ctx.fillStyle = '#f0f0ee' const ax = frameX + innerPad + halfW + gap
ctx.fillRect(halfW + 1.5, photoY, halfW - 1.5, photoH) ctx.fillStyle = '#eef6f1'
ctx.fillStyle = '#bbbbb5' ctx.fillRect(ax, photoY, halfW, photoH)
ctx.fillStyle = '#94a3b8'
ctx.font = `14px ${FONT}` ctx.font = `14px ${FONT}`
ctx.textAlign = 'center' ctx.textAlign = 'center'
ctx.fillText('暂无照片', halfW + halfW / 2, photoY + photoH / 2 + 5) ctx.fillText('洗后', ax + halfW / 2, photoY + photoH / 2)
} }
// 白色中间分隔 // 中间分隔亮条(比纯黑块柔和)
ctx.fillStyle = '#ffffff' ctx.restore()
ctx.fillRect(halfW - 1.5, photoY, 3, photoH) ctx.fillStyle = 'rgba(255,255,255,0.95)'
ctx.fillRect(frameX + innerPad + halfW - gap / 2, photoY, gap, photoH)
// Before / After 标签 // 中间缝上的小标:偏小、偏下,少挡脸
const tagW = 90 const cx = POSTER_W / 2
const tagH = 28 const cy = photoY + Math.floor(photoH * 0.58)
const tagY = photoY + photoH - 12 - tagH const badgeR = 30
ctx.save()
ctx.shadowColor = 'rgba(45, 185, 109, 0.2)'
ctx.shadowBlur = 12
fillRR(ctx, cx - badgeR, cy - badgeR, badgeR * 2, badgeR * 2, badgeR, 'rgba(255,255,255,0.96)')
ctx.restore()
ctx.strokeStyle = 'rgba(45, 185, 109, 0.4)'
ctx.lineWidth = 2
roundRect(ctx, cx - badgeR, cy - badgeR, badgeR * 2, badgeR * 2, badgeR)
ctx.stroke()
fillRR(ctx, 12, tagY, tagW, tagH, 6, 'rgba(245, 145, 62, 0.9)') ctx.fillStyle = '#15803d'
ctx.fillStyle = '#fff' ctx.font = `bold 20px ${FONT}`
ctx.font = `bold 13px ${FONT}`
ctx.textAlign = 'center' ctx.textAlign = 'center'
ctx.fillText('BEFORE', 12 + tagW / 2, tagY + 19) ctx.fillText('✨', cx, cy - 2)
fillRR(ctx, halfW + 12, tagY, tagW, tagH, 6, 'rgba(45, 185, 109, 0.9)')
ctx.fillStyle = '#fff'
ctx.fillText('AFTER', halfW + 12 + tagW / 2, tagY + 19)
// 中间对比提示
fillRR(ctx, POSTER_W / 2 - 38, photoY + photoH / 2 - 16, 76, 32, 16, 'rgba(26, 26, 26, 0.82)')
ctx.fillStyle = '#fff'
ctx.font = `bold 12px ${FONT}` ctx.font = `bold 12px ${FONT}`
ctx.fillText('前后对比', POSTER_W / 2, photoY + photoH / 2 + 5) ctx.fillStyle = '#166534'
ctx.fillText('蜕变', cx, cy + 14)
// 数量信息 // 洗前 / 洗后 角标(中文更接地气)
const metaY = photoY + photoH + 18 const tagW = 100
const metaW = (POSTER_W - PAD * 2 - 12) / 2 const tagH = 34
const metaRightX = PAD + metaW + 12 const tagY = photoY + photoH - 16 - tagH
fillRR(ctx, PAD, metaY, metaW, 42, 12, '#fff7f0') fillRR(ctx, frameX + innerPad + 14, tagY, tagW, tagH, 10, 'rgba(245, 158, 11, 0.92)')
fillRR(ctx, metaRightX, metaY, metaW, 42, 12, '#f3fbf6') ctx.fillStyle = '#fff'
ctx.font = `bold 14px ${FONT}` ctx.font = `bold 15px ${FONT}`
ctx.fillStyle = '#f5913e' ctx.textAlign = 'center'
ctx.fillText(`服务前 ${beforeCount || 0}`, PAD + metaW / 2, metaY + 27) ctx.fillText('洗 前', frameX + innerPad + 14 + tagW / 2, tagY + 23)
ctx.fillStyle = '#2db96d'
ctx.fillText(`服务后 ${afterCount || 0}`, metaRightX + metaW / 2, metaY + 27)
// ── 白色内容卡 ── const ax0 = frameX + innerPad + halfW + gap
const cardY = metaY + 58 fillRR(ctx, ax0 + halfW - tagW - 14, tagY, tagW, tagH, 10, 'rgba(45, 185, 109, 0.92)')
const cardX = PAD ctx.fillStyle = '#fff'
const cardW = POSTER_W - PAD * 2 ctx.fillText('洗 后', ax0 + halfW - 14 - tagW / 2, tagY + 23)
const cardH = POSTER_H - cardY - 100
fillRR(ctx, cardX, cardY, cardW, cardH, 20, '#ffffff')
// 标题与宠物信息 // ── 下方:只保留情绪与宠物名,弱化服务项目 ──
ctx.textAlign = 'left' let textY = photoY + photoH + innerPad + 52
let cy = cardY + 36
ctx.fillStyle = '#1a1a1a' ctx.textAlign = 'center'
ctx.font = `bold 32px ${FONT}` ctx.fillStyle = '#0f172a'
ctx.fillText(petName || '—', cardX + 28, cy) ctx.font = `bold 44px ${FONT}`
cy += 12 ctx.fillText(pet, POSTER_W / 2, textY)
textY += 46
ctx.fillStyle = '#999993' ctx.fillStyle = '#64748b'
ctx.font = `14px ${FONT}` ctx.font = `16px ${FONT}`
ctx.fillText(serviceType || '', cardX + 28, cy + 20) ctx.fillText('护理前后 · 精神又可爱 ✨', POSTER_W / 2, textY)
cy += 48 textY += 36
// 信息卡标题 // 二维码(若业务后续传入)
ctx.fillStyle = '#999993'
ctx.font = `bold 12px ${FONT}`
ctx.fillText('服务详情', cardX + 28, cy - 6)
// 分隔线
ctx.fillStyle = '#f0f0ee'
ctx.fillRect(cardX + 28, cy, cardW - 56, 1)
cy += 20
// 信息行
const infoRows = [
['时间', timeStr || '—'],
['技师', staffName || '—']
]
ctx.font = `14px ${FONT}`
infoRows.forEach(([k, v]) => {
ctx.fillStyle = '#bbbbb5'
ctx.fillText(k, cardX + 28, cy)
ctx.fillStyle = '#1a1a1a'
ctx.font = `bold 15px ${FONT}`
ctx.fillText(String(v), cardX + 100, cy)
ctx.font = `14px ${FONT}`
cy += 36
})
// 备注
if (remark && String(remark).trim()) {
cy += 8
ctx.fillStyle = '#f5f5f2'
roundRect(ctx, cardX + 20, cy - 6, cardW - 40, 80, 12)
ctx.fill()
ctx.fillStyle = '#666660'
ctx.font = `13px ${FONT}`
wrapText(ctx, String(remark).trim(), cardX + 34, cy + 16, cardW - 72, 20, 3)
cy += 92
}
// 二维码(可选)
if (qrImg) { if (qrImg) {
const qrSize = 120 const qrSize = 108
const qx = POSTER_W / 2 - qrSize / 2 const qx = POSTER_W / 2 - qrSize / 2
const qy = cy + 10 const qy = textY + 8
ctx.save() ctx.save()
roundRect(ctx, qx, qy, qrSize, qrSize, 10) roundRect(ctx, qx, qy, qrSize, qrSize, 12)
ctx.clip() ctx.clip()
ctx.drawImage(qrImg, qx, qy, qrSize, qrSize) ctx.drawImage(qrImg, qx, qy, qrSize, qrSize)
ctx.restore() ctx.restore()
cy = qy + qrSize + 10 textY = qy + qrSize + 22
ctx.fillStyle = '#bbbbb5' ctx.fillStyle = '#94a3b8'
ctx.font = `12px ${FONT}` ctx.font = `12px ${FONT}`
ctx.textAlign = 'center' ctx.fillText('长按识别 · 查看完整报告', POSTER_W / 2, textY)
ctx.fillText('扫码查看报告详情', POSTER_W / 2, cy + 4) textY += 36
} }
// ── 底部品牌 ── // ── 底部品牌(轻)──
const footY = POSTER_H - 56 const footY = POSTER_H - 64
ctx.textAlign = 'center' ctx.textAlign = 'center'
ctx.strokeStyle = 'rgba(45, 185, 109, 0.25)'
ctx.lineWidth = 1
ctx.beginPath()
ctx.moveTo(POSTER_W / 2 - 40, footY)
ctx.lineTo(POSTER_W / 2 + 40, footY)
ctx.stroke()
ctx.fillStyle = '#ebebea' ctx.fillStyle = 'rgba(30, 41, 59, 0.85)'
ctx.fillRect(POSTER_W / 2 - 16, footY, 32, 2) ctx.font = `600 15px ${FONT}`
ctx.fillStyle = '#1a1a1a'
ctx.font = `bold 16px ${FONT}`
ctx.fillText(name, POSTER_W / 2, footY + 26) ctx.fillText(name, POSTER_W / 2, footY + 26)
ctx.fillStyle = '#bbbbb5' ctx.fillStyle = 'rgba(148, 163, 184, 0.95)'
ctx.font = `11px ${FONT}` ctx.font = `11px ${FONT}`
ctx.fillText('宠物服务 · 让爱更专业', POSTER_W / 2, footY + 44) ctx.fillText('宠物洗护美容 · 值得晒一晒', POSTER_W / 2, footY + 46)
} }