feat: 宠物档案及报告分享优化
This commit is contained in:
parent
c14767db90
commit
0a41ea62cd
@ -41,7 +41,7 @@
|
|||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<view v-else class="pet-card">
|
<view v-else class="pet-card" hover-class="pet-card--hover" @click="openEdit(p)">
|
||||||
<view class="pet-row">
|
<view class="pet-row">
|
||||||
<view class="pet-avatar-wrap">
|
<view class="pet-avatar-wrap">
|
||||||
<image v-if="p.avatar" :src="imgUrl(p.avatar)" class="pet-avatar" mode="aspectFill" />
|
<image v-if="p.avatar" :src="imgUrl(p.avatar)" class="pet-avatar" mode="aspectFill" />
|
||||||
@ -134,7 +134,7 @@ const isCustomer = computed(() => userInfo.role === 'customer')
|
|||||||
|
|
||||||
const heroSub = computed(() => {
|
const heroSub = computed(() => {
|
||||||
if (isCustomer.value) return '管理您的宠物档案,创建预约时可关联,方便门店识别。'
|
if (isCustomer.value) return '管理您的宠物档案,创建预约时可关联,方便门店识别。'
|
||||||
return '以下为在本店预约中曾关联过的宠物(客户需在预约里选择档案)。'
|
return '以下为在本店预约中曾关联过的宠物。点击卡片可上传头像或编辑档案。'
|
||||||
})
|
})
|
||||||
|
|
||||||
const navSafeStyle = (() => {
|
const navSafeStyle = (() => {
|
||||||
@ -440,6 +440,9 @@ onShow(() => loadPets())
|
|||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
box-shadow: 0 1px 3px rgba(0,0,0,0.04);
|
box-shadow: 0 1px 3px rgba(0,0,0,0.04);
|
||||||
}
|
}
|
||||||
|
.pet-card--hover {
|
||||||
|
opacity: 0.92;
|
||||||
|
}
|
||||||
.pet-row {
|
.pet-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
|
|||||||
@ -268,7 +268,7 @@ import { ref, computed, onMounted, getCurrentInstance, nextTick } from 'vue'
|
|||||||
import { onLoad, onShareAppMessage, onShareTimeline } from '@dcloudio/uni-app'
|
import { onLoad, onShareAppMessage, onShareTimeline } from '@dcloudio/uni-app'
|
||||||
import { getReportByToken, imgUrl } from '../../api/index.js'
|
import { getReportByToken, imgUrl } from '../../api/index.js'
|
||||||
import { formatDateTimeYMDHM } from '../../utils/datetime.js'
|
import { formatDateTimeYMDHM } from '../../utils/datetime.js'
|
||||||
import { drawReportPoster, POSTER_W, POSTER_H } from '../../utils/reportPosterDraw.js'
|
import { drawReportPoster, POSTER_W, computePosterLogicalHeight } from '../../utils/reportPosterDraw.js'
|
||||||
import AppIcon from '../../components/AppIcon.vue'
|
import AppIcon from '../../components/AppIcon.vue'
|
||||||
import { isLoggedIn } from '../../utils/session.js'
|
import { isLoggedIn } from '../../utils/session.js'
|
||||||
|
|
||||||
@ -437,8 +437,9 @@ const generatePoster = async () => {
|
|||||||
const canvas = posterCanvas.value
|
const canvas = posterCanvas.value
|
||||||
if (!canvas) return
|
if (!canvas) return
|
||||||
const ctx = canvas.getContext('2d')
|
const ctx = canvas.getContext('2d')
|
||||||
|
const posterH = computePosterLogicalHeight(false)
|
||||||
canvas.width = POSTER_W
|
canvas.width = POSTER_W
|
||||||
canvas.height = POSTER_H
|
canvas.height = posterH
|
||||||
drawReportPoster(ctx, {
|
drawReportPoster(ctx, {
|
||||||
storeName: r.store?.name || '',
|
storeName: r.store?.name || '',
|
||||||
storePhone: r.store?.phone || '',
|
storePhone: r.store?.phone || '',
|
||||||
@ -557,8 +558,9 @@ const generatePoster = async () => {
|
|||||||
if (win && win.pixelRatio != null) dpr = Math.min(win.pixelRatio, 3)
|
if (win && win.pixelRatio != null) dpr = Math.min(win.pixelRatio, 3)
|
||||||
else dpr = Math.min(uni.getSystemInfoSync().pixelRatio || 2, 3)
|
else dpr = Math.min(uni.getSystemInfoSync().pixelRatio || 2, 3)
|
||||||
} catch (_) { dpr = 2 }
|
} catch (_) { dpr = 2 }
|
||||||
|
const posterH = computePosterLogicalHeight(false)
|
||||||
canvas.width = POSTER_W * dpr
|
canvas.width = POSTER_W * dpr
|
||||||
canvas.height = POSTER_H * dpr
|
canvas.height = posterH * dpr
|
||||||
ctx.scale(dpr, dpr)
|
ctx.scale(dpr, dpr)
|
||||||
|
|
||||||
const beforeImg = await loadCanvasImage(canvas, beforePath)
|
const beforeImg = await loadCanvasImage(canvas, beforePath)
|
||||||
@ -591,7 +593,7 @@ const generatePoster = async () => {
|
|||||||
fileType: 'png',
|
fileType: 'png',
|
||||||
quality: 1,
|
quality: 1,
|
||||||
destWidth: POSTER_W,
|
destWidth: POSTER_W,
|
||||||
destHeight: POSTER_H,
|
destHeight: posterH,
|
||||||
success: (out) => resolve(out.tempFilePath),
|
success: (out) => resolve(out.tempFilePath),
|
||||||
fail: (e) => reject(e)
|
fail: (e) => reject(e)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,14 +1,30 @@
|
|||||||
/**
|
/**
|
||||||
* 洗美报告分享海报 — 朋友圈向:大图对比为主,弱化服务信息
|
* 洗美报告分享海报 — 朋友圈向:大图对比为主,高度随内容收紧,避免底部大块留白
|
||||||
* H5 / 小程序 canvas 2d 共用
|
* H5 / 小程序 canvas 2d 共用
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export const POSTER_W = 750
|
export const POSTER_W = 750
|
||||||
/** 竖版比例偏手机屏,利于发朋友圈 */
|
/** 兼容旧引用:实际高度请用 computePosterLogicalHeight */
|
||||||
export const POSTER_H = 1180
|
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'
|
||||||
|
|
||||||
|
/** 绘制前设置 canvas 高度用(与 drawReportPoster 内布局常量保持一致) */
|
||||||
|
export function computePosterLogicalHeight(hasQr) {
|
||||||
|
const PAD = 28
|
||||||
|
const frameY = 98
|
||||||
|
const innerPad = 10
|
||||||
|
const photoH = 520
|
||||||
|
const photoY = frameY + innerPad
|
||||||
|
const petBaseline = photoY + photoH + innerPad + 28
|
||||||
|
const subBaseline = petBaseline + 40
|
||||||
|
let contentBottom = subBaseline + 22
|
||||||
|
if (hasQr) contentBottom = subBaseline + 24 + 108 + 22
|
||||||
|
const dividerY = contentBottom + 18
|
||||||
|
const sloganY = dividerY + 40
|
||||||
|
return sloganY + 18
|
||||||
|
}
|
||||||
|
|
||||||
function roundRect(ctx, x, y, w, h, r) {
|
function roundRect(ctx, x, y, w, h, r) {
|
||||||
const rr = Math.min(r, w / 2, h / 2)
|
const rr = Math.min(r, w / 2, h / 2)
|
||||||
ctx.beginPath()
|
ctx.beginPath()
|
||||||
@ -26,34 +42,49 @@ function fillRR(ctx, x, y, w, h, r, fill) {
|
|||||||
ctx.fill()
|
ctx.fill()
|
||||||
}
|
}
|
||||||
|
|
||||||
function drawBgGradient(ctx) {
|
function drawBgGradient(ctx, height) {
|
||||||
const g = ctx.createLinearGradient(0, 0, 0, POSTER_H)
|
const g = ctx.createLinearGradient(0, 0, 0, height)
|
||||||
g.addColorStop(0, '#fffefb')
|
g.addColorStop(0, '#fffefb')
|
||||||
g.addColorStop(0.42, '#f4faf6')
|
g.addColorStop(0.42, '#f4faf6')
|
||||||
g.addColorStop(1, '#e5f4ec')
|
g.addColorStop(1, '#e5f4ec')
|
||||||
ctx.fillStyle = g
|
ctx.fillStyle = g
|
||||||
ctx.fillRect(0, 0, POSTER_W, POSTER_H)
|
ctx.fillRect(0, 0, POSTER_W, height)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {CanvasRenderingContext2D} ctx
|
* @param {CanvasRenderingContext2D} ctx
|
||||||
|
* @returns {number} 实际画布高度(生成图片、canvas 尺寸用)
|
||||||
*/
|
*/
|
||||||
export function drawReportPoster(ctx, data) {
|
export function drawReportPoster(ctx, data) {
|
||||||
const {
|
const { storeName, petName, beforeImg, afterImg, qrImg } = data
|
||||||
storeName,
|
|
||||||
petName,
|
|
||||||
beforeImg,
|
|
||||||
afterImg,
|
|
||||||
qrImg
|
|
||||||
} = data
|
|
||||||
|
|
||||||
const name = storeName || '宠伴生活馆'
|
const name = storeName || '宠伴生活馆'
|
||||||
const PAD = 28
|
const PAD = 28
|
||||||
const pet = petName || '宝贝'
|
const pet = petName || '宝贝'
|
||||||
|
|
||||||
drawBgGradient(ctx)
|
const frameX = PAD
|
||||||
|
const frameY = 98
|
||||||
|
const frameW = POSTER_W - PAD * 2
|
||||||
|
const frameR = 28
|
||||||
|
const innerPad = 10
|
||||||
|
const photoY = frameY + innerPad
|
||||||
|
const photoH = 520
|
||||||
|
const gap = 6
|
||||||
|
const innerW = frameW - innerPad * 2
|
||||||
|
const halfW = (innerW - gap) / 2
|
||||||
|
|
||||||
// ── 顶部:轻量品牌(不抢镜)──
|
const posterHeight = computePosterLogicalHeight(!!qrImg)
|
||||||
|
const petBaseline = photoY + photoH + innerPad + 28
|
||||||
|
const subBaseline = petBaseline + 40
|
||||||
|
let contentBottom = subBaseline + 22
|
||||||
|
if (qrImg) contentBottom = subBaseline + 24 + 108 + 22
|
||||||
|
const dividerY = contentBottom + 18
|
||||||
|
const shopNameY = dividerY + 22
|
||||||
|
const sloganY = dividerY + 40
|
||||||
|
|
||||||
|
drawBgGradient(ctx, posterHeight)
|
||||||
|
|
||||||
|
// ── 顶部品牌 ──
|
||||||
ctx.textAlign = 'center'
|
ctx.textAlign = 'center'
|
||||||
ctx.fillStyle = 'rgba(22, 101, 52, 0.88)'
|
ctx.fillStyle = 'rgba(22, 101, 52, 0.88)'
|
||||||
ctx.font = `600 22px ${FONT}`
|
ctx.font = `600 22px ${FONT}`
|
||||||
@ -63,19 +94,7 @@ export function drawReportPoster(ctx, data) {
|
|||||||
ctx.font = `13px ${FONT}`
|
ctx.font = `13px ${FONT}`
|
||||||
ctx.fillText('洗护美容 · 真实对比', POSTER_W / 2, 76)
|
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.save()
|
||||||
ctx.shadowColor = 'rgba(15, 23, 42, 0.08)'
|
ctx.shadowColor = 'rgba(15, 23, 42, 0.08)'
|
||||||
ctx.shadowBlur = 24
|
ctx.shadowBlur = 24
|
||||||
@ -121,12 +140,10 @@ export function drawReportPoster(ctx, data) {
|
|||||||
ctx.fillText('洗后', ax + halfW / 2, photoY + photoH / 2)
|
ctx.fillText('洗后', ax + halfW / 2, photoY + photoH / 2)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 中间分隔亮条(比纯黑块柔和)
|
|
||||||
ctx.restore()
|
ctx.restore()
|
||||||
ctx.fillStyle = 'rgba(255,255,255,0.95)'
|
ctx.fillStyle = 'rgba(255,255,255,0.95)'
|
||||||
ctx.fillRect(frameX + innerPad + halfW - gap / 2, photoY, gap, photoH)
|
ctx.fillRect(frameX + innerPad + halfW - gap / 2, photoY, gap, photoH)
|
||||||
|
|
||||||
// 中间缝上的小标:偏小、偏下,少挡脸
|
|
||||||
const cx = POSTER_W / 2
|
const cx = POSTER_W / 2
|
||||||
const cy = photoY + Math.floor(photoH * 0.58)
|
const cy = photoY + Math.floor(photoH * 0.58)
|
||||||
const badgeR = 30
|
const badgeR = 30
|
||||||
@ -148,7 +165,6 @@ export function drawReportPoster(ctx, data) {
|
|||||||
ctx.fillStyle = '#166534'
|
ctx.fillStyle = '#166534'
|
||||||
ctx.fillText('蜕变', cx, cy + 14)
|
ctx.fillText('蜕变', cx, cy + 14)
|
||||||
|
|
||||||
// 洗前 / 洗后 角标(中文更接地气)
|
|
||||||
const tagW = 100
|
const tagW = 100
|
||||||
const tagH = 34
|
const tagH = 34
|
||||||
const tagY = photoY + photoH - 16 - tagH
|
const tagY = photoY + photoH - 16 - tagH
|
||||||
@ -163,52 +179,46 @@ export function drawReportPoster(ctx, data) {
|
|||||||
ctx.fillStyle = '#fff'
|
ctx.fillStyle = '#fff'
|
||||||
ctx.fillText('洗 后', ax0 + halfW - 14 - tagW / 2, tagY + 23)
|
ctx.fillText('洗 后', ax0 + halfW - 14 - tagW / 2, tagY + 23)
|
||||||
|
|
||||||
// ── 下方:只保留情绪与宠物名,弱化服务项目 ──
|
// ── 文案 ──
|
||||||
let textY = photoY + photoH + innerPad + 52
|
|
||||||
|
|
||||||
ctx.textAlign = 'center'
|
ctx.textAlign = 'center'
|
||||||
ctx.fillStyle = '#0f172a'
|
ctx.fillStyle = '#0f172a'
|
||||||
ctx.font = `bold 44px ${FONT}`
|
ctx.font = `bold 44px ${FONT}`
|
||||||
ctx.fillText(pet, POSTER_W / 2, textY)
|
ctx.fillText(pet, POSTER_W / 2, petBaseline)
|
||||||
textY += 46
|
|
||||||
|
|
||||||
ctx.fillStyle = '#64748b'
|
ctx.fillStyle = '#64748b'
|
||||||
ctx.font = `16px ${FONT}`
|
ctx.font = `16px ${FONT}`
|
||||||
ctx.fillText('护理前后 · 精神又可爱 ✨', POSTER_W / 2, textY)
|
ctx.fillText('护理前后 · 精神又可爱 ✨', POSTER_W / 2, subBaseline)
|
||||||
textY += 36
|
|
||||||
|
|
||||||
// 二维码(若业务后续传入)
|
|
||||||
if (qrImg) {
|
if (qrImg) {
|
||||||
const qrSize = 108
|
const qrSize = 108
|
||||||
|
const qy = subBaseline + 24
|
||||||
const qx = POSTER_W / 2 - qrSize / 2
|
const qx = POSTER_W / 2 - qrSize / 2
|
||||||
const qy = textY + 8
|
|
||||||
ctx.save()
|
ctx.save()
|
||||||
roundRect(ctx, qx, qy, qrSize, qrSize, 12)
|
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()
|
||||||
textY = qy + qrSize + 22
|
|
||||||
ctx.fillStyle = '#94a3b8'
|
ctx.fillStyle = '#94a3b8'
|
||||||
ctx.font = `12px ${FONT}`
|
ctx.font = `12px ${FONT}`
|
||||||
ctx.fillText('长按识别 · 查看完整报告', POSTER_W / 2, textY)
|
ctx.fillText('长按识别 · 查看完整报告', POSTER_W / 2, qy + qrSize + 22)
|
||||||
textY += 36
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── 底部品牌(轻)──
|
// ── 底部品牌(紧跟内容,不用 POSTER_H 硬顶底)──
|
||||||
const footY = POSTER_H - 64
|
|
||||||
ctx.textAlign = 'center'
|
ctx.textAlign = 'center'
|
||||||
ctx.strokeStyle = 'rgba(45, 185, 109, 0.25)'
|
ctx.strokeStyle = 'rgba(45, 185, 109, 0.25)'
|
||||||
ctx.lineWidth = 1
|
ctx.lineWidth = 1
|
||||||
ctx.beginPath()
|
ctx.beginPath()
|
||||||
ctx.moveTo(POSTER_W / 2 - 40, footY)
|
ctx.moveTo(POSTER_W / 2 - 40, dividerY)
|
||||||
ctx.lineTo(POSTER_W / 2 + 40, footY)
|
ctx.lineTo(POSTER_W / 2 + 40, dividerY)
|
||||||
ctx.stroke()
|
ctx.stroke()
|
||||||
|
|
||||||
ctx.fillStyle = 'rgba(30, 41, 59, 0.85)'
|
ctx.fillStyle = 'rgba(30, 41, 59, 0.85)'
|
||||||
ctx.font = `600 15px ${FONT}`
|
ctx.font = `600 15px ${FONT}`
|
||||||
ctx.fillText(name, POSTER_W / 2, footY + 26)
|
ctx.fillText(name, POSTER_W / 2, shopNameY)
|
||||||
|
|
||||||
ctx.fillStyle = 'rgba(148, 163, 184, 0.95)'
|
ctx.fillStyle = 'rgba(148, 163, 184, 0.95)'
|
||||||
ctx.font = `11px ${FONT}`
|
ctx.font = `11px ${FONT}`
|
||||||
ctx.fillText('宠物洗护美容 · 值得晒一晒', POSTER_W / 2, footY + 46)
|
ctx.fillText('宠物洗护美容 · 值得晒一晒', POSTER_W / 2, sloganY)
|
||||||
|
|
||||||
|
return posterHeight
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user