417 lines
12 KiB
Vue
417 lines
12 KiB
Vue
<template>
|
||
<view class="report-view">
|
||
<!-- 加载中 -->
|
||
<view v-if="loading" class="loading-wrap">
|
||
<view class="loading-inner">
|
||
<view class="loading-spinner"></view>
|
||
<text class="loading-text">加载报告中...</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 未找到 -->
|
||
<view v-else-if="notFound" class="not-found">
|
||
<text class="not-found-icon">🔍</text>
|
||
<text class="not-found-text">报告不存在或链接已失效</text>
|
||
</view>
|
||
|
||
<!-- 报告内容 -->
|
||
<view v-else-if="reportData" class="report-content">
|
||
<!-- 品牌头部 -->
|
||
<view class="brand-header">
|
||
<view class="brand-logo">{{ reportData.store?.name || '宠伴生活馆' }}</view>
|
||
<view class="brand-sub">宠物服务,让爱更专业</view>
|
||
<view class="brand-contact" v-if="reportData.store?.phone || reportData.store?.address">
|
||
<text v-if="reportData.store?.phone">📞 {{ reportData.store.phone }}</text>
|
||
<text v-if="reportData.store?.address">📍 {{ reportData.store.address }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 报告标题 -->
|
||
<view class="report-title-wrap">
|
||
<view class="report-title">服务报告</view>
|
||
<view class="report-time">{{ formatTime(reportData.appointmentTime) }}</view>
|
||
</view>
|
||
|
||
<!-- 服务信息 -->
|
||
<view class="service-info">
|
||
<view class="info-row">
|
||
<text class="info-label">宠物名字</text>
|
||
<text class="info-val">{{ reportData.petName || '-' }}</text>
|
||
</view>
|
||
<view class="info-row">
|
||
<text class="info-label">服务项目</text>
|
||
<text class="info-val">{{ reportData.serviceType || '-' }}</text>
|
||
</view>
|
||
<view class="info-row">
|
||
<text class="info-label">服务时间</text>
|
||
<text class="info-val">{{ formatTime(reportData.appointmentTime) }}</text>
|
||
</view>
|
||
<view class="info-row">
|
||
<text class="info-label">服务技师</text>
|
||
<text class="info-val">{{ reportData.staffName || '-' }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 照片对比 -->
|
||
<view class="photo-section">
|
||
<view class="section-label">服务前后对比</view>
|
||
<view class="photo-grid">
|
||
<view class="photo-cell">
|
||
<text class="photo-label">服务前</text>
|
||
<image
|
||
v-if="reportData.beforePhoto"
|
||
:src="imgUrl(reportData.beforePhoto)"
|
||
class="photo-img"
|
||
mode="aspectFill"
|
||
/>
|
||
<view v-else class="photo-empty">暂无照片</view>
|
||
</view>
|
||
<view class="photo-cell">
|
||
<text class="photo-label">服务后</text>
|
||
<image
|
||
v-if="reportData.afterPhoto"
|
||
:src="imgUrl(reportData.afterPhoto)"
|
||
class="photo-img"
|
||
mode="aspectFill"
|
||
/>
|
||
<view v-else class="photo-empty">暂无照片</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 备注 -->
|
||
<view v-if="reportData.remark" class="remark-section">
|
||
<view class="section-label">备注</view>
|
||
<view class="remark-content">{{ reportData.remark }}</view>
|
||
</view>
|
||
|
||
<!-- 生成海报按钮 -->
|
||
<view class="action-section">
|
||
<view class="btn-share" @click="generatePoster">📸 生成图片分享朋友圈</view>
|
||
<view class="btn-book" @click="goHome">我要预约</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- Canvas海报(隐藏) -->
|
||
<canvas canvas-id="posterCanvas" id="posterCanvas" class="poster-canvas" />
|
||
</view>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, onMounted } from 'vue'
|
||
import { getReportByToken, imgUrl } from '../../utils/api.js'
|
||
|
||
const loading = ref(true)
|
||
const notFound = ref(false)
|
||
const reportData = ref(null)
|
||
|
||
const formatTime = (time) => {
|
||
if (!time) return ''
|
||
const d = new Date(time)
|
||
return `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')} ${String(d.getHours()).padStart(2,'0')}:${String(d.getMinutes()).padStart(2,'0')}`
|
||
}
|
||
|
||
const loadReport = async () => {
|
||
const pages = getCurrentPages()
|
||
const currentPage = pages[pages.length - 1]
|
||
const token = currentPage?.options?.token || ''
|
||
|
||
if (!token) {
|
||
notFound.value = true
|
||
loading.value = false
|
||
return
|
||
}
|
||
|
||
const res = await getReportByToken(token)
|
||
loading.value = false
|
||
if (res.code === 200) {
|
||
reportData.value = res.data
|
||
} else {
|
||
notFound.value = true
|
||
}
|
||
}
|
||
|
||
const loadImage = (src) => {
|
||
return new Promise((resolve) => {
|
||
const img = new Image()
|
||
img.crossOrigin = 'anonymous'
|
||
img.onload = () => resolve(img)
|
||
img.onerror = () => resolve(null)
|
||
img.src = src
|
||
})
|
||
}
|
||
|
||
const goHome = () => {
|
||
uni.switchTab({ url: '/pages/home/home' })
|
||
}
|
||
|
||
const generatePoster = async () => {
|
||
if (!reportData.value) return
|
||
uni.showLoading({ title: '生成中...' })
|
||
const r = reportData.value
|
||
const canvas = uni.createCanvasContext('posterCanvas')
|
||
|
||
canvas.width = 750
|
||
canvas.height = 1100
|
||
|
||
// 背景
|
||
canvas.setFillStyle('#ffffff')
|
||
canvas.fillRect(0, 0, 750, 1100)
|
||
|
||
// 顶部绿色
|
||
canvas.setFillStyle('#07c160')
|
||
canvas.fillRect(0, 0, 750, 300)
|
||
|
||
const storeName = r.store?.name || '宠伴生活馆'
|
||
|
||
// Logo
|
||
canvas.setFillStyle('#ffffff')
|
||
canvas.setFontSize(36)
|
||
canvas.setTextAlign('center')
|
||
canvas.fillText(storeName, 375, 70)
|
||
canvas.setFontSize(20)
|
||
canvas.setGlobalAlpha(0.7)
|
||
canvas.fillText('宠物服务,让爱更专业', 375, 105)
|
||
canvas.setGlobalAlpha(1)
|
||
|
||
// 联系信息
|
||
const storePhone = r.store?.phone || ''
|
||
const storeAddr = r.store?.address || ''
|
||
if (storePhone || storeAddr) {
|
||
canvas.setFontSize(18)
|
||
canvas.setGlobalAlpha(0.85)
|
||
const contactLine = [storePhone, storeAddr].filter(Boolean).join(' | ')
|
||
canvas.fillText(contactLine, 375, 138)
|
||
canvas.setGlobalAlpha(1)
|
||
}
|
||
|
||
// 报告标题
|
||
canvas.setFillStyle('#333333')
|
||
canvas.setFontSize(36)
|
||
canvas.fillText('服务报告', 375, 220)
|
||
|
||
// 服务信息卡片
|
||
canvas.setFillStyle('#f8f6f3')
|
||
roundRect(canvas, 40, 260, 670, 220, 20)
|
||
canvas.fill()
|
||
|
||
const infoItems = [
|
||
['宠物名字', r.petName || '-'],
|
||
['服务项目', r.serviceType || '-'],
|
||
['服务时间', formatTime(r.appointmentTime) || '-'],
|
||
['服务技师', r.staffName || '-']
|
||
]
|
||
let y = 310
|
||
canvas.setTextAlign('left')
|
||
infoItems.forEach(([label, val]) => {
|
||
canvas.setFillStyle('#999999')
|
||
canvas.setFontSize(22)
|
||
canvas.fillText(label, 80, y)
|
||
canvas.setFillStyle('#333333')
|
||
canvas.setFontSize(24)
|
||
canvas.fillText(val, 220, y)
|
||
y += 48
|
||
})
|
||
|
||
// 照片区域
|
||
canvas.setFillStyle('#f8f6f3')
|
||
roundRect(canvas, 40, 500, 670, 360, 20)
|
||
canvas.fill()
|
||
|
||
canvas.setFillStyle('#333333')
|
||
canvas.setFontSize(24)
|
||
canvas.setTextAlign('center')
|
||
canvas.fillText('服务前后对比', 375, 545)
|
||
|
||
const imgY = 575
|
||
const imgH = 260
|
||
const imgW = 300
|
||
|
||
// 服务前占位
|
||
canvas.setFillStyle('#e0e0e0')
|
||
roundRect(canvas, 60, imgY, imgW, imgH, 16)
|
||
canvas.fill()
|
||
canvas.setFillStyle('#999999')
|
||
canvas.setFontSize(20)
|
||
canvas.fillText('服务前', 210, imgY + imgH/2)
|
||
|
||
// 服务后占位
|
||
canvas.setFillStyle('#e0e0e0')
|
||
roundRect(canvas, 390, imgY, imgW, imgH, 16)
|
||
canvas.fill()
|
||
canvas.setFillStyle('#999999')
|
||
canvas.fillText('服务后', 540, imgY + imgH/2)
|
||
|
||
// 备注
|
||
if (r.remark) {
|
||
canvas.setFillStyle('#f8f6f3')
|
||
roundRect(canvas, 40, 880, 670, 100, 20)
|
||
canvas.fill()
|
||
canvas.setFillStyle('#666666')
|
||
canvas.setFontSize(22)
|
||
canvas.setTextAlign('left')
|
||
const remark = r.remark
|
||
if (remark.length > 30) {
|
||
canvas.fillText(remark.substring(0, 30), 70, 920)
|
||
canvas.fillText(remark.substring(30), 70, 955)
|
||
} else {
|
||
canvas.fillText(remark, 70, 930)
|
||
}
|
||
}
|
||
|
||
// 底部Logo
|
||
canvas.setFillStyle('#07c160')
|
||
canvas.setFontSize(22)
|
||
canvas.setTextAlign('center')
|
||
canvas.fillText(`— ${storeName} —`, 375, 1050)
|
||
|
||
canvas.draw()
|
||
|
||
// 异步绘制真实照片
|
||
const beforeImgSrc = r.beforePhoto ? imgUrl(r.beforePhoto) : null
|
||
const afterImgSrc = r.afterPhoto ? imgUrl(r.afterPhoto) : null
|
||
const [beforeImg, afterImg] = await Promise.all([
|
||
beforeImgSrc ? loadImage(beforeImgSrc) : Promise.resolve(null),
|
||
afterImgSrc ? loadImage(afterImgSrc) : Promise.resolve(null)
|
||
])
|
||
|
||
if (beforeImg) {
|
||
canvas.drawImage(beforeImg, 60, imgY, imgW, imgH)
|
||
canvas.draw()
|
||
}
|
||
if (afterImg) {
|
||
canvas.drawImage(afterImg, 390, imgY, imgW, imgH)
|
||
canvas.draw()
|
||
}
|
||
|
||
uni.hideLoading()
|
||
uni.showToast({ title: '海报已生成,请截图分享', icon: 'none', duration: 3000 })
|
||
}
|
||
|
||
function roundRect(ctx, x, y, w, h, r) {
|
||
ctx.beginPath()
|
||
ctx.moveTo(x + r, y)
|
||
ctx.lineTo(x + w - r, y)
|
||
ctx.arcTo(x + w, y, x + w, y + r, r)
|
||
ctx.lineTo(x + w, y + h - r)
|
||
ctx.arcTo(x + w, y + h, x + w - r, y + h, r)
|
||
ctx.lineTo(x + r, y + h)
|
||
ctx.arcTo(x, y + h, x, y + h - r, r)
|
||
ctx.lineTo(x, y + r)
|
||
ctx.arcTo(x, y, x + r, y, r)
|
||
ctx.closePath()
|
||
}
|
||
|
||
onMounted(() => loadReport())
|
||
</script>
|
||
|
||
<style scoped>
|
||
.report-view { background: #f8f6f3; min-height: 100vh; }
|
||
.loading-wrap {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
min-height: 100vh;
|
||
}
|
||
.loading-inner { display: flex; flex-direction: column; align-items: center; gap: 16px; }
|
||
.loading-spinner {
|
||
width: 36px;
|
||
height: 36px;
|
||
border: 3px solid #07c160;
|
||
border-top-color: transparent;
|
||
border-radius: 50%;
|
||
animation: spin 0.8s linear infinite;
|
||
}
|
||
@keyframes spin { to { transform: rotate(360deg); } }
|
||
.loading-text { font-size: 14px; color: #999; }
|
||
.not-found { display: flex; flex-direction: column; align-items: center; justify-content: center; min-height: 100vh; gap: 12px; }
|
||
.not-found-icon { font-size: 60px; }
|
||
.not-found-text { font-size: 15px; color: #999; }
|
||
|
||
.report-content { max-width: 430px; margin: 0 auto; background: #fff; min-height: 100vh; }
|
||
.brand-header {
|
||
background: linear-gradient(135deg, #07c160 0%, #10b76f 100%);
|
||
padding: 24px 24px 20px;
|
||
text-align: center;
|
||
color: #fff;
|
||
}
|
||
.brand-logo { font-size: 20px; font-weight: 700; letter-spacing: 2px; margin-bottom: 4px; }
|
||
.brand-sub { font-size: 12px; opacity: 0.7; margin-bottom: 12px; }
|
||
.brand-contact { font-size: 12px; opacity: 0.85; display: flex; justify-content: center; gap: 16px; flex-wrap: wrap; }
|
||
.report-title-wrap { text-align: center; padding: 24px 24px 16px; }
|
||
.report-title { font-size: 22px; font-weight: 700; color: #333; }
|
||
.report-time { font-size: 13px; color: #999; margin-top: 6px; }
|
||
|
||
.service-info {
|
||
margin: 0 16px 16px;
|
||
background: #f8f6f3;
|
||
border-radius: 12px;
|
||
padding: 4px 16px;
|
||
}
|
||
.info-row {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
padding: 12px 0;
|
||
border-bottom: 1px solid #f0f0f0;
|
||
}
|
||
.info-row:last-child { border-bottom: none; }
|
||
.info-label { font-size: 14px; color: #999; }
|
||
.info-val { font-size: 14px; color: #333; font-weight: 500; }
|
||
|
||
.photo-section { margin: 0 24px 24px; }
|
||
.section-label { font-size: 15px; font-weight: 600; color: #333; margin-bottom: 12px; }
|
||
.photo-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
|
||
.photo-cell { display: flex; flex-direction: column; gap: 8px; }
|
||
.photo-label { font-size: 13px; color: #999; text-align: center; }
|
||
.photo-img { width: 100%; height: 160px; border-radius: 12px; object-fit: cover; }
|
||
.photo-empty {
|
||
width: 100%;
|
||
height: 160px;
|
||
background: #f0f0f0;
|
||
border-radius: 12px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
color: #ccc;
|
||
font-size: 13px;
|
||
}
|
||
|
||
.remark-section { margin: 0 24px 24px; }
|
||
.remark-content { background: #f9f9f9; border-radius: 12px; padding: 16px; font-size: 14px; color: #666; line-height: 1.6; min-height: 60px; }
|
||
.action-section { margin: 0 24px 24px; }
|
||
.btn-share {
|
||
width: 100%;
|
||
height: 46px;
|
||
background: #07c160;
|
||
color: #fff;
|
||
border-radius: 23px;
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
margin-bottom: 12px;
|
||
}
|
||
.btn-book {
|
||
width: 100%;
|
||
height: 46px;
|
||
background: #fff;
|
||
color: #07c160;
|
||
border: 1px solid #07c160;
|
||
border-radius: 23px;
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.poster-canvas {
|
||
position: fixed;
|
||
top: -9999px;
|
||
left: -9999px;
|
||
width: 750px;
|
||
height: 1100px;
|
||
}
|
||
</style>
|