推拿小程序前端代码仓库
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

563 lines
13 KiB

<template>
<view class="placard">
<view class="placard-content">
<!-- H5环境显示最终图片 -->
<view v-if="tempFilePath" class="img-box" :style="{ width: canvasW + 'px', height: canvasH + 'px' }">
<img
:src="tempFilePath"
:style="{ width: canvasW + 'px', height: canvasH + 'px' }"
@contextmenu.prevent
style="display: block; border-radius: 10px;"
/>
</view>
<!-- 生成中的提示 -->
<view v-else class="loading-box" :style="{ width: canvasW + 'px', height: canvasH + 'px' }">
<view class="loading-content">
<view class="loading-spinner"></view>
<text class="loading-text">
{{ !userId ? '获取用户信息中...' : '海报生成中...' }}
</text>
</view>
</view>
<!-- 二维码组件 - 隐藏 -->
<view v-if="userId" class="qrcode" ref="qrcode">
<uv-qrcode
:value="qrCodeValue"
:size="qrCodeSize"
type="image/png"
ref="qrCodeComponent"
@complete="onQRCodeComplete">
</uv-qrcode>
</view>
<!-- 画布 - 隐藏 -->
<canvas
:style="{ width: canvasW + 'px', height: canvasH + 'px' }"
canvas-id="myCanvas"
id="myCanvas"
:width="canvasW"
:height="canvasH"
class="hidden-canvas">
</canvas>
<view class="add-btn">
<view class="btn" @click="handleSaveImage">
长按图片保存到手机
</view>
</view>
</view>
</view>
</template>
<script>
import Vue from 'vue'
import { mapState } from 'vuex'
export default {
name: 'Placard',
computed: {
...mapState(['userInfo']),
// 获取用户ID
userId() {
return this.userInfo?.id || ''
},
// 动态生成二维码内容
qrCodeContent() {
if (this.userId) {
return Vue.prototype.$config.redirect + `?vid=${this.userId}`
}
return Vue.prototype.$config.redirect
}
},
data() {
return {
qrCodeValue: '', // 初始为空,等用户信息加载后再设置
qrCodeSize: 180,
qrCodeDarkColor: '#000',
qrCodeLightColor: '#fff',
margin: 0,
//画布信息
canvasW: 299,
canvasH: 403,
//设备信息
systemInfo: {},
//图片路径
tempFilePath: '',
_rpx: 1, // H5环境使用固定比例
_center: 0,
// 二维码是否生成完成
qrCodeReady: false,
// 重试次数
retryCount: 0,
maxRetry: 1,
// 用户信息是否已加载
userInfoLoaded: false
}
},
watch: {
// 监听用户信息变化
userInfo: {
handler(newVal) {
if (newVal && newVal.id) {
this.userInfoLoaded = true
this.qrCodeValue = this.qrCodeContent
// 如果二维码还未生成,现在生成
if (!this.qrCodeReady) {
this.$nextTick(() => {
this.generateQRCode()
})
}
}
},
deep: true,
immediate: true
}
},
mounted() {
this.initCanvas()
},
onShow() {
// 页面显示时确保获取用户信息
this.getUserInfo()
},
methods: {
// 初始化画布
initCanvas() {
// H5环境使用固定尺寸,便于显示
this.canvasW = 299
this.canvasH = 403
this.qrCodeSize = 180
this._center = this.canvasW / 2
// 检查用户信息是否已加载
if (this.userInfoLoaded && this.qrCodeValue) {
this.generateQRCode()
} else {
// 获取用户信息
this.getUserInfo()
}
},
// 获取用户信息
getUserInfo() {
// 如果store中没有用户信息,尝试获取
if (!this.userInfo?.id) {
this.$store.commit('getUserInfo')
}
},
// 生成二维码
generateQRCode() {
// 确保有用户ID才生成二维码
if (!this.userId) {
console.log('等待用户信息加载...')
return
}
// 确保二维码内容已设置
if (!this.qrCodeValue) {
this.qrCodeValue = this.qrCodeContent
}
console.log('生成二维码,内容:', this.qrCodeValue)
this.$nextTick(() => {
if (this.$refs.qrCodeComponent) {
this.$refs.qrCodeComponent.make()
}
})
},
// 二维码生成完成回调
onQRCodeComplete(res) {
if (res.success) {
this.qrCodeReady = true
this.draw()
}
},
// 绘制海报
async draw() {
if (!this.qrCodeReady) {
console.log('二维码未准备好')
return
}
const ctx = uni.createCanvasContext('myCanvas', this)
// 设置背景色
ctx.setFillStyle('#ffffff')
ctx.fillRect(0, 0, this.canvasW, this.canvasH)
// 绘制标题
ctx.setFillStyle('#333333')
ctx.setFontSize(18)
ctx.setTextAlign('center')
ctx.fillText('邀请好友一起享受优质服务', this.canvasW / 2, 30)
// 绘制用户头像(如果有)
if (this.userInfo.headImage) {
try {
// 先下载图片到本地
const avatarPath = await this.downloadImage(this.userInfo.headImage)
const avatarSize = 60
const avatarX = (this.canvasW - avatarSize) / 2
const avatarY = 50
// 绘制圆形头像背景
ctx.setFillStyle('#f0f0f0')
ctx.beginPath()
ctx.arc(avatarX + avatarSize / 2, avatarY + avatarSize / 2, avatarSize / 2, 0, 2 * Math.PI)
ctx.fill()
// 绘制圆形头像
ctx.save()
ctx.beginPath()
ctx.arc(avatarX + avatarSize / 2, avatarY + avatarSize / 2, avatarSize / 2, 0, 2 * Math.PI)
ctx.clip()
ctx.drawImage(avatarPath, avatarX, avatarY, avatarSize, avatarSize)
ctx.restore()
} catch (e) {
console.log('头像绘制失败', e)
// 绘制默认头像圆圈
const avatarSize = 60
const avatarX = (this.canvasW - avatarSize) / 2
const avatarY = 50
ctx.setFillStyle('#e0e0e0')
ctx.beginPath()
ctx.arc(avatarX + avatarSize / 2, avatarY + avatarSize / 2, avatarSize / 2, 0, 2 * Math.PI)
ctx.fill()
}
} else {
// 没有头像时绘制默认头像圆圈
const avatarSize = 60
const avatarX = (this.canvasW - avatarSize) / 2
const avatarY = 50
ctx.setFillStyle('#e0e0e0')
ctx.beginPath()
ctx.arc(avatarX + avatarSize / 2, avatarY + avatarSize / 2, avatarSize / 2, 0, 2 * Math.PI)
ctx.fill()
}
// 绘制用户昵称
if (this.userInfo.nickName) {
ctx.setFillStyle('#333333')
ctx.setFontSize(16)
ctx.setTextAlign('center')
ctx.fillText(this.userInfo.nickName || '用户', this.canvasW / 2, 140)
}
// 获取二维码图片并绘制
try {
const qrCodeImagePath = await this.getQRCodeImage()
if (qrCodeImagePath) {
const qrSize = 120
const qrX = (this.canvasW - qrSize) / 2
const qrY = 160
ctx.drawImage(qrCodeImagePath, qrX, qrY, qrSize, qrSize)
}
} catch (e) {
console.log('二维码绘制失败', e)
}
// 绘制底部文案
ctx.setFillStyle('#666666')
ctx.setFontSize(14)
ctx.setTextAlign('center')
ctx.fillText('扫码关注,享受专业服务', this.canvasW / 2, 310)
// 执行绘制
ctx.draw(false, () => {
// 导出图片
setTimeout(() => {
this.canvasToTempFilePath()
}, 1000)
})
},
// 下载网络图片到本地
downloadImage(url) {
return new Promise((resolve, reject) => {
// H5环境下直接使用网络图片URL,避免跨域问题
// #ifdef H5
resolve(url)
// #endif
// #ifndef H5
uni.downloadFile({
url: url,
success: (res) => {
if (res.statusCode === 200) {
resolve(res.tempFilePath)
} else {
reject('下载失败')
}
},
fail: reject
})
// #endif
})
},
// 获取二维码图片
getQRCodeImage() {
return new Promise((resolve, reject) => {
if (this.$refs.qrCodeComponent) {
this.$refs.qrCodeComponent.toTempFilePath({
success: (res) => {
resolve(res.tempFilePath)
},
fail: (err) => {
reject(err)
}
})
} else {
reject('二维码组件不存在')
}
})
},
// 画布导出为图片
canvasToTempFilePath() {
// #ifdef H5
// H5环境下直接转换canvas
setTimeout(() => {
this.convertCanvasToBase64()
}, 500)
// #endif
// #ifndef H5
uni.canvasToTempFilePath({
canvasId: 'myCanvas',
success: (res) => {
this.tempFilePath = res.tempFilePath
console.log('海报生成成功', res.tempFilePath)
},
fail: (err) => {
console.log('海报生成失败', err)
uni.showToast({
title: '海报生成失败',
icon: 'none'
})
}
}, this)
// #endif
},
// 将canvas转换为base64图片
convertCanvasToBase64() {
// #ifdef H5
try {
// 获取canvas元素
const canvasElement = document.querySelector('#myCanvas')
if (!canvasElement) {
console.error('找不到canvas元素')
this.retryConvert()
return
}
// 检查是否是canvas元素
if (canvasElement.tagName.toLowerCase() !== 'canvas') {
console.error('元素不是canvas')
this.retryConvert()
return
}
// 等待canvas绘制完成
setTimeout(() => {
try {
// 转换为base64
const dataURL = canvasElement.toDataURL('image/png', 1.0)
if (dataURL && dataURL.startsWith('data:image')) {
this.tempFilePath = dataURL
console.log('海报生成成功')
this.retryCount = 0 // 重置重试次数
} else {
throw new Error('生成的图片数据无效')
}
} catch (e) {
console.error('转换图片失败', e)
this.retryConvert()
}
}, 200)
} catch (e) {
console.error('转换图片失败', e)
this.retryConvert()
}
// #endif
},
// 重试转换
retryConvert() {
if (this.retryCount < this.maxRetry) {
this.retryCount++
console.log(`重试转换图片,第${this.retryCount}`)
setTimeout(() => {
this.convertCanvasToBase64()
}, 500)
} else {
console.log('重试次数已达上限,使用备用方法')
this.fallbackCanvasConvert()
}
},
// 备用的canvas转换方法
fallbackCanvasConvert() {
uni.canvasToTempFilePath({
canvasId: 'myCanvas',
success: (res) => {
this.tempFilePath = res.tempFilePath
console.log('海报生成成功(备用方法)', res.tempFilePath)
},
fail: (err) => {
console.log('海报生成失败', err)
uni.showToast({
title: '海报生成失败',
icon: 'none'
})
}
}, this)
},
// 保存图片 - H5环境提示用户长按
handleSaveImage() {
if (!this.tempFilePath) {
uni.showToast({
title: '图片还未生成完成',
icon: 'none'
})
return
}
// H5环境下提示用户长按保存
uni.showModal({
title: '保存图片',
content: '请长按上方图片,选择"保存图片"即可保存到手机',
showCancel: false,
confirmText: '知道了'
})
}
}
}
</script>
<style lang="scss" scoped>
.placard {
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
background-color: #f5f5f5;
padding: 20px;
.placard-content {
display: flex;
flex-direction: column;
align-items: center;
.img-box {
background-color: #fff;
border-radius: 10px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
overflow: hidden;
margin-bottom: 20px;
img {
user-select: none;
-webkit-user-select: none;
-webkit-touch-callout: default;
}
}
.loading-box {
background-color: #fff;
border-radius: 10px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
margin-bottom: 20px;
display: flex;
align-items: center;
justify-content: center;
.loading-content {
display: flex;
flex-direction: column;
align-items: center;
.loading-spinner {
width: 30px;
height: 30px;
border: 3px solid #f3f3f3;
border-top: 3px solid #84A73F;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 10px;
}
.loading-text {
color: #666;
font-size: 14px;
}
}
}
.add-btn {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
.btn {
display: flex;
align-items: center;
justify-content: center;
width: 80%;
height: 40px;
border-radius: 20px;
color: white;
font-size: 16px;
background: linear-gradient(to right, #84A73F, #D8FF8F);
margin-top: 20px;
box-shadow: 0 4px 12px rgba(132, 167, 63, 0.3);
cursor: pointer;
&:active {
transform: scale(0.98);
transition: transform 0.1s;
}
}
}
}
}
.hidden-canvas {
opacity: 0;
position: fixed;
top: -9999px;
left: -9999px;
pointer-events: none;
}
.qrcode {
position: fixed;
top: -9999px;
left: -9999px;
opacity: 0;
pointer-events: none;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
</style>