fix: 修复报告分享页
This commit is contained in:
parent
3be6d0bf9b
commit
f855675f09
@ -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(() => {
|
||||||
|
|||||||
@ -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
|
||||||
|
|
||||||
fillRR(ctx, 12, tagY, tagW, tagH, 6, 'rgba(245, 145, 62, 0.9)')
|
|
||||||
ctx.fillStyle = '#fff'
|
|
||||||
ctx.font = `bold 13px ${FONT}`
|
|
||||||
ctx.textAlign = 'center'
|
|
||||||
ctx.fillText('BEFORE', 12 + tagW / 2, tagY + 19)
|
|
||||||
|
|
||||||
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.fillText('前后对比', POSTER_W / 2, photoY + photoH / 2 + 5)
|
|
||||||
|
|
||||||
// 数量信息
|
|
||||||
const metaY = photoY + photoH + 18
|
|
||||||
const metaW = (POSTER_W - PAD * 2 - 12) / 2
|
|
||||||
const metaRightX = PAD + metaW + 12
|
|
||||||
fillRR(ctx, PAD, metaY, metaW, 42, 12, '#fff7f0')
|
|
||||||
fillRR(ctx, metaRightX, metaY, metaW, 42, 12, '#f3fbf6')
|
|
||||||
ctx.font = `bold 14px ${FONT}`
|
|
||||||
ctx.fillStyle = '#f5913e'
|
|
||||||
ctx.fillText(`服务前 ${beforeCount || 0} 张`, PAD + metaW / 2, metaY + 27)
|
|
||||||
ctx.fillStyle = '#2db96d'
|
|
||||||
ctx.fillText(`服务后 ${afterCount || 0} 张`, metaRightX + metaW / 2, metaY + 27)
|
|
||||||
|
|
||||||
// ── 白色内容卡 ──
|
|
||||||
const cardY = metaY + 58
|
|
||||||
const cardX = PAD
|
|
||||||
const cardW = POSTER_W - PAD * 2
|
|
||||||
const cardH = POSTER_H - cardY - 100
|
|
||||||
fillRR(ctx, cardX, cardY, cardW, cardH, 20, '#ffffff')
|
|
||||||
|
|
||||||
// 标题与宠物信息
|
|
||||||
ctx.textAlign = 'left'
|
|
||||||
let cy = cardY + 36
|
|
||||||
|
|
||||||
ctx.fillStyle = '#1a1a1a'
|
|
||||||
ctx.font = `bold 32px ${FONT}`
|
|
||||||
ctx.fillText(petName || '—', cardX + 28, cy)
|
|
||||||
cy += 12
|
|
||||||
|
|
||||||
ctx.fillStyle = '#999993'
|
|
||||||
ctx.font = `14px ${FONT}`
|
|
||||||
ctx.fillText(serviceType || '', cardX + 28, cy + 20)
|
|
||||||
cy += 48
|
|
||||||
|
|
||||||
// 信息卡标题
|
|
||||||
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) {
|
|
||||||
const qrSize = 120
|
|
||||||
const qx = POSTER_W / 2 - qrSize / 2
|
|
||||||
const qy = cy + 10
|
|
||||||
ctx.save()
|
ctx.save()
|
||||||
roundRect(ctx, qx, qy, qrSize, qrSize, 10)
|
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()
|
||||||
|
|
||||||
|
ctx.fillStyle = '#15803d'
|
||||||
|
ctx.font = `bold 20px ${FONT}`
|
||||||
|
ctx.textAlign = 'center'
|
||||||
|
ctx.fillText('✨', cx, cy - 2)
|
||||||
|
ctx.font = `bold 12px ${FONT}`
|
||||||
|
ctx.fillStyle = '#166534'
|
||||||
|
ctx.fillText('蜕变', cx, cy + 14)
|
||||||
|
|
||||||
|
// 洗前 / 洗后 角标(中文更接地气)
|
||||||
|
const tagW = 100
|
||||||
|
const tagH = 34
|
||||||
|
const tagY = photoY + photoH - 16 - tagH
|
||||||
|
fillRR(ctx, frameX + innerPad + 14, tagY, tagW, tagH, 10, 'rgba(245, 158, 11, 0.92)')
|
||||||
|
ctx.fillStyle = '#fff'
|
||||||
|
ctx.font = `bold 15px ${FONT}`
|
||||||
|
ctx.textAlign = 'center'
|
||||||
|
ctx.fillText('洗 前', frameX + innerPad + 14 + tagW / 2, tagY + 23)
|
||||||
|
|
||||||
|
const ax0 = frameX + innerPad + halfW + gap
|
||||||
|
fillRR(ctx, ax0 + halfW - tagW - 14, tagY, tagW, tagH, 10, 'rgba(45, 185, 109, 0.92)')
|
||||||
|
ctx.fillStyle = '#fff'
|
||||||
|
ctx.fillText('洗 后', ax0 + halfW - 14 - tagW / 2, tagY + 23)
|
||||||
|
|
||||||
|
// ── 下方:只保留情绪与宠物名,弱化服务项目 ──
|
||||||
|
let textY = photoY + photoH + innerPad + 52
|
||||||
|
|
||||||
|
ctx.textAlign = 'center'
|
||||||
|
ctx.fillStyle = '#0f172a'
|
||||||
|
ctx.font = `bold 44px ${FONT}`
|
||||||
|
ctx.fillText(pet, POSTER_W / 2, textY)
|
||||||
|
textY += 46
|
||||||
|
|
||||||
|
ctx.fillStyle = '#64748b'
|
||||||
|
ctx.font = `16px ${FONT}`
|
||||||
|
ctx.fillText('护理前后 · 精神又可爱 ✨', POSTER_W / 2, textY)
|
||||||
|
textY += 36
|
||||||
|
|
||||||
|
// 二维码(若业务后续传入)
|
||||||
|
if (qrImg) {
|
||||||
|
const qrSize = 108
|
||||||
|
const qx = POSTER_W / 2 - qrSize / 2
|
||||||
|
const qy = textY + 8
|
||||||
|
ctx.save()
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user