<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>
|