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