diff --git a/src/pages/report-view/reportView.vue b/src/pages/report-view/reportView.vue index 52b71db..8248532 100644 --- a/src/pages/report-view/reportView.vue +++ b/src/pages/report-view/reportView.vue @@ -307,11 +307,10 @@ const afterMorePhotos = computed(() => afterPhotos.value.slice(1)) const shareTitle = computed(() => { const r = reportData.value - if (!r) return '宠物洗护美容报告' + if (!r) return '洗护前后对比' const store = r.store?.name || '宠伴生活馆' const pet = r.petName || '爱宠' - const svc = r.serviceType || '洗护美容' - return `${store}|${pet}的${svc}报告` + return `${pet}的洗护蜕变|${store}` }) const shareCoverUrl = computed(() => { diff --git a/src/utils/reportPosterDraw.js b/src/utils/reportPosterDraw.js index e674574..162ad64 100644 --- a/src/utils/reportPosterDraw.js +++ b/src/utils/reportPosterDraw.js @@ -1,10 +1,11 @@ /** - * 洗美报告分享海报 — 简约风格 v2 + * 洗美报告分享海报 — 朋友圈向:大图对比为主,弱化服务信息 * H5 / 小程序 canvas 2d 共用 */ 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' @@ -25,29 +26,13 @@ function fillRR(ctx, x, y, w, h, r, fill) { ctx.fill() } -function wrapText(ctx, text, x, y, maxW, lineH, maxLines) { - if (!text) return y - const chars = String(text).split('') - let line = '' - let cy = y - let n = 0 - for (let i = 0; i < chars.length; i++) { - 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 +function drawBgGradient(ctx) { + const g = ctx.createLinearGradient(0, 0, 0, POSTER_H) + g.addColorStop(0, '#fffefb') + g.addColorStop(0.42, '#f4faf6') + g.addColorStop(1, '#e5f4ec') + ctx.fillStyle = g + ctx.fillRect(0, 0, POSTER_W, POSTER_H) } /** @@ -55,201 +40,175 @@ function wrapText(ctx, text, x, y, maxW, lineH, maxLines) { */ export function drawReportPoster(ctx, data) { const { - storeName, storePhone, storeAddr, - petName, serviceType, timeStr, staffName, - remark, beforeImg, afterImg, qrImg, - beforeCount, afterCount + storeName, + petName, + beforeImg, + afterImg, + qrImg } = data const name = storeName || '宠伴生活馆' - const PAD = 40 + const PAD = 28 + const pet = petName || '宝贝' - // ── 背景 ── - ctx.fillStyle = '#fafaf8' - ctx.fillRect(0, 0, POSTER_W, POSTER_H) - - // ── 顶部品牌条 ── - const barH = 100 - ctx.fillStyle = '#1a1a1a' - ctx.fillRect(0, 0, POSTER_W, barH) + drawBgGradient(ctx) + // ── 顶部:轻量品牌(不抢镜)── ctx.textAlign = 'center' - ctx.fillStyle = '#ffffff' - ctx.font = `bold 30px ${FONT}` - ctx.fillText(name, POSTER_W / 2, 60) + ctx.fillStyle = 'rgba(22, 101, 52, 0.88)' + ctx.font = `600 22px ${FONT}` + ctx.fillText(name, POSTER_W / 2, 48) - // ── 照片对比区 ── - const photoY = barH - const photoH = 360 - const halfW = POSTER_W / 2 + ctx.fillStyle = 'rgba(100, 116, 139, 0.85)' + ctx.font = `13px ${FONT}` + ctx.fillText('洗护美容 · 真实对比', POSTER_W / 2, 76) + + // ── 主视觉:大尺寸双图 + 相框感 ── + 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) { - ctx.save() - ctx.beginPath() - ctx.rect(0, photoY, halfW - 1.5, photoH) - ctx.clip() const bw = beforeImg.width || halfW const bh = beforeImg.height || photoH const scale = Math.max(halfW / bw, photoH / bh) const dw = bw * scale const dh = bh * scale - ctx.drawImage(beforeImg, (halfW - dw) / 2, photoY + (photoH - dh) / 2, dw, dh) - ctx.restore() + ctx.drawImage(beforeImg, frameX + innerPad + (halfW - dw) / 2, photoY + (photoH - dh) / 2, dw, dh) } else { - ctx.fillStyle = '#f0f0ee' - ctx.fillRect(0, photoY, halfW - 1.5, photoH) - ctx.fillStyle = '#bbbbb5' + ctx.fillStyle = '#eef2f0' + ctx.fillRect(frameX + innerPad, photoY, halfW, photoH) + ctx.fillStyle = '#94a3b8' ctx.font = `14px ${FONT}` ctx.textAlign = 'center' - ctx.fillText('暂无照片', halfW / 2, photoY + photoH / 2 + 5) + ctx.fillText('洗前', frameX + innerPad + halfW / 2, photoY + photoH / 2) } if (afterImg) { - ctx.save() - ctx.beginPath() - ctx.rect(halfW + 1.5, photoY, halfW - 1.5, photoH) - ctx.clip() + const ax = frameX + innerPad + halfW + gap const aw = afterImg.width || halfW const ah = afterImg.height || photoH const scale = Math.max(halfW / aw, photoH / ah) const dw = aw * scale const dh = ah * scale - ctx.drawImage(afterImg, halfW + 1.5 + (halfW - dw) / 2, photoY + (photoH - dh) / 2, dw, dh) - ctx.restore() + ctx.drawImage(afterImg, ax + (halfW - dw) / 2, photoY + (photoH - dh) / 2, dw, dh) } else { - ctx.fillStyle = '#f0f0ee' - ctx.fillRect(halfW + 1.5, photoY, halfW - 1.5, photoH) - ctx.fillStyle = '#bbbbb5' + const ax = frameX + innerPad + halfW + gap + ctx.fillStyle = '#eef6f1' + ctx.fillRect(ax, photoY, halfW, photoH) + ctx.fillStyle = '#94a3b8' ctx.font = `14px ${FONT}` ctx.textAlign = 'center' - ctx.fillText('暂无照片', halfW + halfW / 2, photoY + photoH / 2 + 5) + ctx.fillText('洗后', ax + halfW / 2, photoY + photoH / 2) } - // 白色中间分隔 - ctx.fillStyle = '#ffffff' - ctx.fillRect(halfW - 1.5, photoY, 3, photoH) + // 中间分隔亮条(比纯黑块柔和) + ctx.restore() + ctx.fillStyle = 'rgba(255,255,255,0.95)' + ctx.fillRect(frameX + innerPad + halfW - gap / 2, photoY, gap, photoH) - // Before / After 标签 - const tagW = 90 - const tagH = 28 - const tagY = photoY + photoH - 12 - tagH + // 中间缝上的小标:偏小、偏下,少挡脸 + const cx = POSTER_W / 2 + const cy = photoY + Math.floor(photoH * 0.58) + 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 = '#fff' - ctx.font = `bold 13px ${FONT}` + ctx.fillStyle = '#15803d' + ctx.font = `bold 20px ${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.fillText('✨', cx, cy - 2) 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 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 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 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') + 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) - // 标题与宠物信息 - ctx.textAlign = 'left' - let cy = cardY + 36 + // ── 下方:只保留情绪与宠物名,弱化服务项目 ── + let textY = photoY + photoH + innerPad + 52 - ctx.fillStyle = '#1a1a1a' - ctx.font = `bold 32px ${FONT}` - ctx.fillText(petName || '—', cardX + 28, cy) - cy += 12 + ctx.textAlign = 'center' + ctx.fillStyle = '#0f172a' + ctx.font = `bold 44px ${FONT}` + ctx.fillText(pet, POSTER_W / 2, textY) + textY += 46 - ctx.fillStyle = '#999993' - ctx.font = `14px ${FONT}` - ctx.fillText(serviceType || '', cardX + 28, cy + 20) - cy += 48 + ctx.fillStyle = '#64748b' + ctx.font = `16px ${FONT}` + ctx.fillText('护理前后 · 精神又可爱 ✨', POSTER_W / 2, textY) + 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) { - const qrSize = 120 + const qrSize = 108 const qx = POSTER_W / 2 - qrSize / 2 - const qy = cy + 10 + const qy = textY + 8 ctx.save() - roundRect(ctx, qx, qy, qrSize, qrSize, 10) + roundRect(ctx, qx, qy, qrSize, qrSize, 12) ctx.clip() ctx.drawImage(qrImg, qx, qy, qrSize, qrSize) ctx.restore() - cy = qy + qrSize + 10 - ctx.fillStyle = '#bbbbb5' + textY = qy + qrSize + 22 + ctx.fillStyle = '#94a3b8' ctx.font = `12px ${FONT}` - ctx.textAlign = 'center' - ctx.fillText('扫码查看报告详情', POSTER_W / 2, cy + 4) + ctx.fillText('长按识别 · 查看完整报告', POSTER_W / 2, textY) + textY += 36 } - // ── 底部品牌 ── - const footY = POSTER_H - 56 + // ── 底部品牌(轻)── + const footY = POSTER_H - 64 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.fillRect(POSTER_W / 2 - 16, footY, 32, 2) - - ctx.fillStyle = '#1a1a1a' - ctx.font = `bold 16px ${FONT}` + ctx.fillStyle = 'rgba(30, 41, 59, 0.85)' + ctx.font = `600 15px ${FONT}` ctx.fillText(name, POSTER_W / 2, footY + 26) - ctx.fillStyle = '#bbbbb5' + ctx.fillStyle = 'rgba(148, 163, 184, 0.95)' ctx.font = `11px ${FONT}` - ctx.fillText('宠物服务 · 让爱更专业', POSTER_W / 2, footY + 44) + ctx.fillText('宠物洗护美容 · 值得晒一晒', POSTER_W / 2, footY + 46) }