537 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			Vue
		
	
	
	
			
		
		
	
	
			537 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			Vue
		
	
	
	
| <template>
 | ||
|   <view
 | ||
|     class="share-page"
 | ||
|     style="display: flex; flex-direction: column; height: 100vh"
 | ||
|   >
 | ||
|     <view id="shareContainer" class="share-container">
 | ||
|       <!-- 微信环境:只有在未生成分享图时才显示原始内容 -->
 | ||
|       <template v-if="!isWechat || (isWechat && !generatedImageUrl)">
 | ||
|         <!-- 背景图片,替代CSS background -->
 | ||
|         <image
 | ||
|           class="share-bg-image"
 | ||
|           src="/static/images/share-bg.jpg"
 | ||
|           mode="scaleToFill"
 | ||
|           crossorigin="anonymous"
 | ||
|           @load="onBackgroundImageLoad"
 | ||
|           @error="onBackgroundImageError"
 | ||
|         ></image>
 | ||
|         <view class="share-wrapper">
 | ||
|           <view class="portal-frame" :class="{ 'is-loaded': isLoaded }">
 | ||
|             <view class="qr-code-outer">
 | ||
|               <image
 | ||
|                 class="qr-code"
 | ||
|                 :src="qrCodeImage"
 | ||
|                 mode="aspectFit"
 | ||
|                 v-if="qrCodeImage"
 | ||
|               ></image>
 | ||
|               <view v-else class="qr-code-placeholder">
 | ||
|                 <view class="loader"></view>
 | ||
|               </view>
 | ||
|             </view>
 | ||
|             <text
 | ||
|               style="
 | ||
|                 font-size: 30rpx;
 | ||
|                 color: #fff;
 | ||
|                 font-weight: bold;
 | ||
|                 margin-top: 20rpx;
 | ||
|               "
 | ||
|               >{{ desensitization(userInfo.memberCode) }}</text
 | ||
|             >
 | ||
|             <button v-if="!isWechat" class="share-button" @click="sharePage">
 | ||
|               {{ isWechat ? '长按保存图片' : '保存图片并分享' }}
 | ||
|             </button>
 | ||
|           </view>
 | ||
|           <!-- <image
 | ||
|             class="share-bg-logo"
 | ||
|             src="/static/images/share-logo.png"
 | ||
|             mode="scaleToFill"
 | ||
|           ></image> -->
 | ||
|         </view>
 | ||
|       </template>
 | ||
| 
 | ||
|       <view
 | ||
|         class="wechat-fullscreen-overlay"
 | ||
|         v-show="isWechat && generatedImageUrl"
 | ||
|         @click="closeFullscreenImage"
 | ||
|       >
 | ||
|         <image
 | ||
|           class="fullscreen-image"
 | ||
|           :src="generatedImageUrl"
 | ||
|           mode="scaleToFill"
 | ||
|           @load="onGeneratedImageLoad"
 | ||
|           @error="onGeneratedImageError"
 | ||
|           @click.stop=""
 | ||
|         ></image>
 | ||
|       </view>
 | ||
|     </view>
 | ||
|     <cl-tabbar class="tabbar" :current="2" />
 | ||
|   </view>
 | ||
| </template>
 | ||
| 
 | ||
| <script>
 | ||
| import html2canvas from 'html2canvas'
 | ||
| import { getShareCode } from '@/config/share'
 | ||
| import clTabbar from '@/components/cl-tabbar.vue'
 | ||
| 
 | ||
| export default {
 | ||
|   name: 'ShareQRCode',
 | ||
|   components: {
 | ||
|     'cl-tabbar': clTabbar,
 | ||
|   },
 | ||
|   data() {
 | ||
|     return {
 | ||
|       qrCodeImage: '',
 | ||
|       // Set canvas dimensions. It's better to get device screen width for this.
 | ||
|       canvasWidth: 375,
 | ||
|       canvasHeight: 800,
 | ||
|       isLoaded: false,
 | ||
|       shareButtonShow: true,
 | ||
|       isWechat: false, // 是否微信环境
 | ||
|       generatedImageUrl: '', // 生成的图片URL,用于微信长按保存
 | ||
|       backgroundImageLoaded: false, // 背景图片是否加载完成
 | ||
|       userInfo: uni.getStorageSync('User'),
 | ||
|     }
 | ||
|   },
 | ||
|   onLoad() {
 | ||
|     this.checkWechatEnvironment()
 | ||
|     this.handleGetShareCode()
 | ||
|     // Get screen width to set canvas width dynamically
 | ||
|     uni.getSystemInfo({
 | ||
|       success: res => {
 | ||
|         this.canvasWidth = res.windowWidth
 | ||
|         this.canvasHeight = res.windowHeight
 | ||
|       },
 | ||
|     })
 | ||
|   },
 | ||
|   onReady() {
 | ||
|     // Use a short timeout to ensure the initial render is complete before animation
 | ||
|     setTimeout(() => {
 | ||
|       this.isLoaded = true
 | ||
|     }, 100)
 | ||
|   },
 | ||
|   methods: {
 | ||
|     desensitization(str) {
 | ||
|       if (!str) return ''
 | ||
|       if (str.length <= 8) return str.slice(0, 4) + '****'
 | ||
|       const len = str.length - 6
 | ||
|       const placeholder = '*'.repeat(len)
 | ||
|       return str.slice(0, 4) + placeholder + str.slice(-2)
 | ||
|     },
 | ||
|     // 检测微信环境
 | ||
|     checkWechatEnvironment() {
 | ||
|       const ua = navigator.userAgent.toLowerCase()
 | ||
|       this.isWechat = ua.includes('micromessenger')
 | ||
|       console.log('微信环境检测:', this.isWechat)
 | ||
|     },
 | ||
| 
 | ||
|     handleGetShareCode() {
 | ||
|       // Don't show loading toast, use the placeholder loader instead
 | ||
|       // uni.showLoading({ title: '加载中...' })
 | ||
|       getShareCode()
 | ||
|         .then(res => {
 | ||
|           // The screenshot shows the base64 string is in data.datStr
 | ||
|           if (res.code === 200 && res.data && res.data.dataStr) {
 | ||
|             this.qrCodeImage = 'data:image/png;base64,' + res.data.dataStr
 | ||
|             this.$nextTick(() => {
 | ||
|               if (this.isWechat) {
 | ||
|                 this.sharePage()
 | ||
|               }
 | ||
|             })
 | ||
|           } else {
 | ||
|             uni.showToast({
 | ||
|               title: '获取分享码失败',
 | ||
|               icon: 'none',
 | ||
|             })
 | ||
|           }
 | ||
|         })
 | ||
|         .catch(err => {
 | ||
|           console.error('getShareCode error:', err)
 | ||
|           uni.showToast({
 | ||
|             title: '网络错误,请稍后再试',
 | ||
|             icon: 'none',
 | ||
|           })
 | ||
|         })
 | ||
|     },
 | ||
| 
 | ||
|     async sharePage() {
 | ||
|       if (!this.qrCodeImage) {
 | ||
|         uni.showToast({
 | ||
|           title: '二维码尚未生成',
 | ||
|           icon: 'none',
 | ||
|         })
 | ||
|         return
 | ||
|       }
 | ||
| 
 | ||
|       // 统一使用html2canvas生成图片
 | ||
|       uni.showLoading({ title: '加载中...' })
 | ||
| 
 | ||
|       try {
 | ||
|         // 隐藏按钮,等待DOM更新
 | ||
|         this.shareButtonShow = false
 | ||
|         await this.$nextTick()
 | ||
| 
 | ||
|         // 使用html2canvas截取页面
 | ||
|         await this.capturePageWithHtml2Canvas()
 | ||
|       } catch (error) {
 | ||
|         uni.hideLoading()
 | ||
|         uni.showToast({ title: '图片生成失败,请稍后重试', icon: 'none' })
 | ||
|         console.error('sharePage error:', error)
 | ||
|       } finally {
 | ||
|         // 恢复按钮显示
 | ||
|         this.shareButtonShow = true
 | ||
|       }
 | ||
|     },
 | ||
| 
 | ||
|     // 生成的图片加载成功
 | ||
|     onGeneratedImageLoad() {
 | ||
|       console.log('生成的图片加载成功')
 | ||
|     },
 | ||
| 
 | ||
|     // 生成的图片加载失败
 | ||
|     onGeneratedImageError(e) {
 | ||
|       console.error('生成的图片加载失败:', e)
 | ||
|       uni.showToast({
 | ||
|         title: '图片显示失败',
 | ||
|         icon: 'none',
 | ||
|       })
 | ||
|     },
 | ||
| 
 | ||
|     // 关闭全屏图片
 | ||
|     closeFullscreenImage() {
 | ||
|       this.generatedImageUrl = ''
 | ||
|       console.log('关闭全屏图片')
 | ||
|     },
 | ||
| 
 | ||
|     // 背景图片加载成功
 | ||
|     onBackgroundImageLoad() {
 | ||
|       this.backgroundImageLoaded = true
 | ||
|       console.log('背景图片加载成功')
 | ||
|     },
 | ||
| 
 | ||
|     // 背景图片加载失败
 | ||
|     onBackgroundImageError(e) {
 | ||
|       console.error('背景图片加载失败:', e)
 | ||
|     },
 | ||
| 
 | ||
|     // 使用html2canvas截取整个页面
 | ||
|     async capturePageWithHtml2Canvas() {
 | ||
|       console.log('开始使用html2canvas截取页面')
 | ||
| 
 | ||
|       return new Promise((resolve, reject) => {
 | ||
|         // 确保所有图片都已加载完成
 | ||
|         const waitForImages = () => {
 | ||
|           if (!this.backgroundImageLoaded || !this.qrCodeImage) {
 | ||
|             setTimeout(waitForImages, 100)
 | ||
|             return
 | ||
|           }
 | ||
| 
 | ||
|           // 额外等待确保渲染完成
 | ||
|           setTimeout(() => {
 | ||
|             const element = document.getElementById('shareContainer')
 | ||
|             if (!element) {
 | ||
|               reject(new Error('找不到页面容器'))
 | ||
|               return
 | ||
|             }
 | ||
| 
 | ||
|             console.log(
 | ||
|               '开始html2canvas截取,容器尺寸:',
 | ||
|               element.offsetWidth,
 | ||
|               'x',
 | ||
|               element.offsetHeight
 | ||
|             )
 | ||
| 
 | ||
|             html2canvas(element, {
 | ||
|               useCORS: true,
 | ||
|               allowTaint: true,
 | ||
|               backgroundColor: null,
 | ||
|               scale: 2,
 | ||
|               dpi: 400,
 | ||
|               logging: true, // 开启日志便于调试
 | ||
|               width: element.offsetWidth,
 | ||
|               height: element.offsetHeight,
 | ||
|               windowWidth: element.offsetWidth,
 | ||
|               windowHeight: element.offsetHeight,
 | ||
|               scrollX: 0,
 | ||
|               scrollY: 0,
 | ||
|               x: 0,
 | ||
|               y: 0,
 | ||
|             })
 | ||
|               .then(canvas => {
 | ||
|                 const dataUrl = canvas.toDataURL('image/jpeg', 1)
 | ||
| 
 | ||
|                 // 根据环境处理结果
 | ||
|                 if (this.isWechat) {
 | ||
|                   // 微信环境:设置图片供长按保存
 | ||
|                   this.generatedImageUrl = dataUrl
 | ||
|                   uni.hideLoading()
 | ||
|                 } else {
 | ||
|                   // 普通浏览器:直接下载图片
 | ||
|                   this.downloadImage(dataUrl)
 | ||
|                   uni.hideLoading()
 | ||
|                   uni.showToast({
 | ||
|                     title: '图片已开始下载',
 | ||
|                     icon: 'success',
 | ||
|                   })
 | ||
|                 }
 | ||
| 
 | ||
|                 resolve()
 | ||
|               })
 | ||
|               .catch(err => {
 | ||
|                 console.error('html2canvas截取失败:', err)
 | ||
|                 reject(err)
 | ||
|               })
 | ||
|           }, 1000) // 增加等待时间到1000ms
 | ||
|         }
 | ||
| 
 | ||
|         // 开始等待图片加载
 | ||
|         waitForImages()
 | ||
|       })
 | ||
|     },
 | ||
| 
 | ||
|     // 原生图片加载器
 | ||
|     loadImage(src) {
 | ||
|       return new Promise((resolve, reject) => {
 | ||
|         const img = new Image()
 | ||
|         img.crossOrigin = 'anonymous'
 | ||
| 
 | ||
|         img.onload = () => {
 | ||
|           console.log(
 | ||
|             `图片加载成功: ${src.substring(0, 50)}...`,
 | ||
|             `${img.width}x${img.height}`
 | ||
|           )
 | ||
|           resolve(img)
 | ||
|         }
 | ||
| 
 | ||
|         img.onerror = error => {
 | ||
|           console.error(`图片加载失败: ${src}`, error)
 | ||
|           reject(new Error(`图片加载失败: ${src}`))
 | ||
|         }
 | ||
| 
 | ||
|         img.src = src
 | ||
|       })
 | ||
|     },
 | ||
| 
 | ||
|     // 下载图片
 | ||
|     downloadImage(dataUrl) {
 | ||
|       const link = document.createElement('a')
 | ||
|       link.href = dataUrl
 | ||
|       link.download = `share_page_${Date.now()}.png`
 | ||
|       document.body.appendChild(link)
 | ||
|       link.click()
 | ||
|       document.body.removeChild(link)
 | ||
|     },
 | ||
|   },
 | ||
| }
 | ||
| </script>
 | ||
| 
 | ||
| <style lang="scss" scoped>
 | ||
| .share-container {
 | ||
|   flex: 1;
 | ||
|   height: 0;
 | ||
|   box-sizing: border-box;
 | ||
|   position: relative;
 | ||
|   overflow: hidden;
 | ||
|   display: flex;
 | ||
|   flex-direction: column;
 | ||
|   align-items: center;
 | ||
| }
 | ||
| 
 | ||
| /* 背景图片样式 */
 | ||
| .share-bg-image {
 | ||
|   position: absolute;
 | ||
|   top: 0;
 | ||
|   left: 0;
 | ||
|   width: 100%;
 | ||
|   height: 100%;
 | ||
|   z-index: 1;
 | ||
| }
 | ||
| 
 | ||
| .share-bg {
 | ||
|   width: 100%;
 | ||
|   height: 100%;
 | ||
|   img {
 | ||
|     width: 100%;
 | ||
|     height: 100%;
 | ||
|   }
 | ||
| }
 | ||
| .share-wrapper {
 | ||
|   position: absolute;
 | ||
|   z-index: 2;
 | ||
|   bottom: 22%;
 | ||
|   display: flex;
 | ||
|   flex-direction: column;
 | ||
|   align-items: center;
 | ||
| }
 | ||
| .share-bg-logo {
 | ||
|   width: 100%;
 | ||
|   height: 360rpx;
 | ||
|   // margin-top: -60rpx;
 | ||
| }
 | ||
| 
 | ||
| .portal-frame {
 | ||
|   padding: 32rpx;
 | ||
|   width: 520rpx;
 | ||
|   border-radius: 40rpx;
 | ||
| 
 | ||
|   display: flex;
 | ||
|   box-sizing: border-box;
 | ||
|   flex-direction: column;
 | ||
|   align-items: center;
 | ||
|   margin: 0 auto;
 | ||
| }
 | ||
| 
 | ||
| .portal-frame.is-loaded {
 | ||
|   opacity: 1;
 | ||
|   transform: translateY(0);
 | ||
| }
 | ||
| 
 | ||
| .title {
 | ||
|   font-size: 44rpx;
 | ||
|   font-weight: 500;
 | ||
|   color: #1e1e1e;
 | ||
|   margin-bottom: 60rpx;
 | ||
| }
 | ||
| 
 | ||
| /* The single white card for the QR code */
 | ||
| .qr-code-outer {
 | ||
|   width: 400rpx; /* 从400rpx缩小到320rpx */
 | ||
|   height: 400rpx; /* 从400rpx缩小到320rpx */
 | ||
|   background: rgba(255, 255, 255, 0.98);
 | ||
|   border-radius: 20rpx; /* 从24rpx减小到20rpx */
 | ||
|   box-shadow: 0px 8rpx 20rpx rgba(50, 50, 90, 0.06);
 | ||
|   border: 1px solid #f0f0f0;
 | ||
|   display: flex;
 | ||
|   align-items: center;
 | ||
|   justify-content: center;
 | ||
|   padding: 12rpx; /* 从16rpx减小到12rpx */
 | ||
|   box-sizing: border-box;
 | ||
| }
 | ||
| 
 | ||
| /* The image and the placeholder both live inside the outer card */
 | ||
| .qr-code,
 | ||
| .qr-code-placeholder {
 | ||
|   width: 100%;
 | ||
|   height: 100%;
 | ||
| }
 | ||
| 
 | ||
| .qr-code {
 | ||
|   border-radius: 16rpx;
 | ||
| }
 | ||
| 
 | ||
| .qr-code-placeholder {
 | ||
|   display: flex;
 | ||
|   align-items: center;
 | ||
|   justify-content: center;
 | ||
| }
 | ||
| 
 | ||
| .loader {
 | ||
|   width: 100rpx;
 | ||
|   height: 100rpx;
 | ||
|   border: 8rpx solid rgba(0, 0, 0, 0.1);
 | ||
|   border-left-color: #0072ff;
 | ||
|   border-radius: 50%;
 | ||
|   animation: spin 1s linear infinite;
 | ||
| }
 | ||
| 
 | ||
| @keyframes spin {
 | ||
|   to {
 | ||
|     transform: rotate(360deg);
 | ||
|   }
 | ||
| }
 | ||
| 
 | ||
| .tip {
 | ||
|   font-size: 30rpx;
 | ||
|   color: #888;
 | ||
| }
 | ||
| 
 | ||
| /* Unified style for both the real button and the fake one (which is a view) */
 | ||
| .share-button {
 | ||
|   margin-top: 28rpx; /* 从32rpx减小到28rpx */
 | ||
|   width: 280rpx; /* 设置固定宽度 */
 | ||
|   height: 72rpx; /* 从88rpx减小到72rpx */
 | ||
|   line-height: 72rpx;
 | ||
|   color: #fff;
 | ||
|   border-radius: 36rpx; /* 从44rpx减小到36rpx */
 | ||
|   font-size: 26rpx; /* 从30rpx减小到26rpx */
 | ||
|   font-weight: 500;
 | ||
|   /* 更精细的渐变 */
 | ||
|   background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
 | ||
|   box-shadow: 0 5rpx 12rpx 0 rgba(102, 126, 234, 0.2);
 | ||
|   border: none;
 | ||
|   padding: 0;
 | ||
|   text-align: center;
 | ||
|   transition: all 0.3s ease;
 | ||
|   letter-spacing: 1rpx;
 | ||
| }
 | ||
| 
 | ||
| /* The real button needs to override some uni-app defaults */
 | ||
| button.share-button {
 | ||
|   padding: 0;
 | ||
|   line-height: 72rpx; /* 更新行高到72rpx */
 | ||
|   border: none;
 | ||
| }
 | ||
| 
 | ||
| .share-button:active {
 | ||
|   transform: translateY(1rpx);
 | ||
|   box-shadow: 0 4rpx 10rpx rgba(102, 126, 234, 0.4);
 | ||
|   background: linear-gradient(
 | ||
|     135deg,
 | ||
|     #5a67d8 0%,
 | ||
|     #6b46c1 100%
 | ||
|   ); /* 按下时的渐变 */
 | ||
| }
 | ||
| 
 | ||
| /* 微信环境全屏覆盖样式 */
 | ||
| .wechat-fullscreen-overlay {
 | ||
|   position: absolute;
 | ||
|   top: 0;
 | ||
|   left: 0;
 | ||
|   width: 100vw;
 | ||
|   height: 100%; /* 减去tab栏高度 */
 | ||
| 
 | ||
|   background-color: transparent; /* 移除背景色 */
 | ||
|   z-index: 999;
 | ||
|   display: flex;
 | ||
|   flex-direction: column;
 | ||
|   align-items: stretch; /* 拉伸对齐 */
 | ||
|   justify-content: stretch; /* 拉伸对齐 */
 | ||
|   padding: 0;
 | ||
|   margin: 0;
 | ||
| }
 | ||
| 
 | ||
| .fullscreen-image {
 | ||
|   width: 100vw;
 | ||
|   height: 100%; /* 减去tab栏高度 */
 | ||
|   object-fit: cover; /* 覆盖整个容器,可能会裁剪 */
 | ||
|   margin: 0;
 | ||
|   padding: 0;
 | ||
|   border: none;
 | ||
|   display: block;
 | ||
| }
 | ||
| @media screen and (max-height: 667px) {
 | ||
|   .tabbar {
 | ||
|     height: 70px !important;
 | ||
|   }
 | ||
| }
 | ||
| 
 | ||
| @media screen and (min-height: 812px) {
 | ||
|   /* iPhone X及以上机型 */
 | ||
|   .tabbar {
 | ||
|     height: 80px !important;
 | ||
|   }
 | ||
| }
 | ||
| .tabbar {
 | ||
|   position: static;
 | ||
|   bottom: 0;
 | ||
|   z-index: 1000;
 | ||
|   width: 100%;
 | ||
|   height: 50px;
 | ||
|   :v-deep .u-tabbar--fixed {
 | ||
|     height: 100px !important;
 | ||
|   }
 | ||
|   // ::v-deep .u-safe-area-inset-bottom {
 | ||
|   //   display: none !important;
 | ||
|   // }
 | ||
| }
 | ||
| </style>
 |