Compare commits
	
		
			3 Commits
		
	
	
		
			2f37181c1d
			...
			3e3399c1ed
		
	
	| Author | SHA1 | Date | 
|---|---|---|
|  | 3e3399c1ed | |
|  | 1ea89b0c68 | |
|  | f0ba57a7a4 | 
|  | @ -240,4 +240,33 @@ export default { | |||
|     // margin-bottom: 30px; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| /* iPhone安全区域适配 */ | ||||
| 
 | ||||
| /* 绿色主题的安全区域适配 */ | ||||
| .greenEd { | ||||
|   ::v-deep .u-tabbar__content { | ||||
|     background: linear-gradient(to bottom, #fff, #b6fdda); | ||||
| 
 | ||||
|     /* 绿色主题的安全区域背景 */ | ||||
|     &::after { | ||||
|       background: #b6fdda; | ||||
|     } | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| /* 针对不同屏幕尺寸的优化 */ | ||||
| @media screen and (max-height: 667px) { | ||||
|   /* iPhone SE等小屏设备 */ | ||||
|   ::v-deep .u-tabbar__content__item-wrapper { | ||||
|     height: 70px; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| @media screen and (min-height: 812px) { | ||||
|   /* iPhone X及以上机型 */ | ||||
|   ::v-deep .u-tabbar__content__item-wrapper { | ||||
|     height: 80px; | ||||
|   } | ||||
| } | ||||
| </style> | ||||
|  |  | |||
|  | @ -19,6 +19,7 @@ | |||
|         "qrcodejs2": "0.0.2", | ||||
|         "swiper": "^3.4.2", | ||||
|         "uqrcodejs": "^4.0.7", | ||||
|         "vconsole": "^3.15.1", | ||||
|         "vue-clipboard2": "^0.3.3", | ||||
|         "vue-i18n": "^9.2.2", | ||||
|         "vue-tree-color": "^2.3.2", | ||||
|  | @ -64,6 +65,15 @@ | |||
|         "node": ">=6.0.0" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/@babel/runtime": { | ||||
|       "version": "7.27.6", | ||||
|       "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.6.tgz", | ||||
|       "integrity": "sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==", | ||||
|       "license": "MIT", | ||||
|       "engines": { | ||||
|         "node": ">=6.9.0" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/@babel/types": { | ||||
|       "version": "7.27.0", | ||||
|       "resolved": "https://mirrors.cloud.tencent.com/npm/@babel/types/-/types-7.27.0.tgz", | ||||
|  | @ -1821,6 +1831,29 @@ | |||
|         "node": ">=0.10.0" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/copy-text-to-clipboard": { | ||||
|       "version": "3.2.0", | ||||
|       "resolved": "https://registry.npmjs.org/copy-text-to-clipboard/-/copy-text-to-clipboard-3.2.0.tgz", | ||||
|       "integrity": "sha512-RnJFp1XR/LOBDckxTib5Qjr/PMfkatD0MUCQgdpqS8MdKiNUzBjAQBEN6oUy+jW7LI93BBG3DtMB2KOOKpGs2Q==", | ||||
|       "license": "MIT", | ||||
|       "engines": { | ||||
|         "node": ">=12" | ||||
|       }, | ||||
|       "funding": { | ||||
|         "url": "https://github.com/sponsors/sindresorhus" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/core-js": { | ||||
|       "version": "3.43.0", | ||||
|       "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.43.0.tgz", | ||||
|       "integrity": "sha512-N6wEbTTZSYOY2rYAn85CuvWWkCK6QweMn7/4Nr3w+gDBeBhk/x4EJeY6FPo4QzDoJZxVTv8U7CMvgWk6pOHHqA==", | ||||
|       "hasInstallScript": true, | ||||
|       "license": "MIT", | ||||
|       "funding": { | ||||
|         "type": "opencollective", | ||||
|         "url": "https://opencollective.com/core-js" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/core-util-is": { | ||||
|       "version": "1.0.3", | ||||
|       "resolved": "https://mirrors.cloud.tencent.com/npm/core-util-is/-/core-util-is-1.0.3.tgz", | ||||
|  | @ -4357,6 +4390,11 @@ | |||
|       "dev": true, | ||||
|       "peer": true | ||||
|     }, | ||||
|     "node_modules/mutation-observer": { | ||||
|       "version": "1.0.3", | ||||
|       "resolved": "https://registry.npmjs.org/mutation-observer/-/mutation-observer-1.0.3.tgz", | ||||
|       "integrity": "sha512-M/O/4rF2h776hV7qGMZUH3utZLO/jK7p8rnNgGkjKUw8zCGjRQPxB8z6+5l8+VjRUQ3dNYu4vjqXYLr+U8ZVNA==" | ||||
|     }, | ||||
|     "node_modules/nan": { | ||||
|       "version": "2.22.2", | ||||
|       "resolved": "https://mirrors.cloud.tencent.com/npm/nan/-/nan-2.22.2.tgz", | ||||
|  | @ -6469,6 +6507,18 @@ | |||
|         "base64-arraybuffer": "^1.0.2" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/vconsole": { | ||||
|       "version": "3.15.1", | ||||
|       "resolved": "https://registry.npmjs.org/vconsole/-/vconsole-3.15.1.tgz", | ||||
|       "integrity": "sha512-KH8XLdrq9T5YHJO/ixrjivHfmF2PC2CdVoK6RWZB4yftMykYIaXY1mxZYAic70vADM54kpMQF+dYmvl5NRNy1g==", | ||||
|       "license": "MIT", | ||||
|       "dependencies": { | ||||
|         "@babel/runtime": "^7.17.2", | ||||
|         "copy-text-to-clipboard": "^3.0.1", | ||||
|         "core-js": "^3.11.0", | ||||
|         "mutation-observer": "^1.0.3" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/vm-browserify": { | ||||
|       "version": "1.1.2", | ||||
|       "resolved": "https://mirrors.cloud.tencent.com/npm/vm-browserify/-/vm-browserify-1.1.2.tgz", | ||||
|  |  | |||
|  | @ -14,6 +14,7 @@ | |||
|     "qrcodejs2": "0.0.2", | ||||
|     "swiper": "^3.4.2", | ||||
|     "uqrcodejs": "^4.0.7", | ||||
|     "vconsole": "^3.15.1", | ||||
|     "vue-clipboard2": "^0.3.3", | ||||
|     "vue-i18n": "^9.2.2", | ||||
|     "vue-tree-color": "^2.3.2", | ||||
|  |  | |||
|  | @ -30,12 +30,18 @@ | |||
|                 userInfo.pkMaxAwardsVal || userInfo.pkAwardsVal || '无' | ||||
|               }}</text> | ||||
|             </view> | ||||
|             <view class="award-tag"> | ||||
|               <!-- <u-icon name="star-fill" color="#FAD65A" size="14"></u-icon> --> | ||||
|               <text class="award-label">当月奖衔:</text> | ||||
|               <text class="award-value">{{ | ||||
|                 userInfo.pkAwardsVal || '无' | ||||
|               }}</text> | ||||
|             <view style="display: flex; gap: 10rpx"> | ||||
|               <view class="award-tag"> | ||||
|                 <!-- <u-icon name="star-fill" color="#FAD65A" size="14"></u-icon> --> | ||||
|                 <text class="award-label">当月奖衔:</text> | ||||
|                 <text class="award-value">{{ | ||||
|                   userInfo.pkAwardsVal || '无' | ||||
|                 }}</text> | ||||
|               </view> | ||||
|               <view v-if="userInfo.pkRangeAwardsVal" class="award-tag"> | ||||
|                 <text class="award-label">分红奖衔:</text> | ||||
|                 <text class="award-value">{{ userInfo.pkRangeAwardsVal }}</text> | ||||
|               </view> | ||||
|             </view> | ||||
|           </view> | ||||
|         </view> | ||||
|  | @ -402,13 +408,13 @@ export default { | |||
|           menuKey: 'selfHelp', | ||||
|           ifshow: true, | ||||
|         }, | ||||
|         { | ||||
|           url: '/pages/mine/share/index', | ||||
|           name: '个人推广', | ||||
|           imgurl: '../../static/images/list.svg', | ||||
|           menuKey: 'share', | ||||
|           ifshow: false, | ||||
|         }, | ||||
|         // { | ||||
|         //   url: '/pages/mine/share/index', | ||||
|         //   name: '个人推广', | ||||
|         //   imgurl: '../../static/images/list.svg', | ||||
|         //   menuKey: 'share', | ||||
|         //   ifshow: false, | ||||
|         // }, | ||||
|         { | ||||
|           url: '/pages/userSecure/index', | ||||
|           name: '账号安全', | ||||
|  | @ -1017,7 +1023,7 @@ export default { | |||
|   padding: 40rpx 0; | ||||
|   color: #ffffff; | ||||
|   position: relative; | ||||
|   padding-bottom: 30rpx; // Add some bottom padding | ||||
|   padding-bottom: 0; // Add some bottom padding | ||||
| } | ||||
| 
 | ||||
| .user-info-wrapper { | ||||
|  | @ -1290,6 +1296,7 @@ export default { | |||
|   box-shadow: 0rpx 6rpx 24rpx rgba(0, 0, 0, 0.06); | ||||
|   border-radius: 24rpx; | ||||
|   padding: 20rpx; | ||||
|   margin-top: 20rpx; | ||||
| } | ||||
| 
 | ||||
| .info-grid { | ||||
|  |  | |||
|  | @ -1,37 +1,70 @@ | |||
| <template> | ||||
|   <view id="shareContainer" class="share-container"> | ||||
|     <!-- Portal Frame: The main content to be saved. --> | ||||
|     <view class="portal-frame" :class="{ 'is-loaded': isLoaded }"> | ||||
|       <!-- The single, robust QR Code card --> | ||||
|       <view class="qr-code-outer"> | ||||
|   <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="qr-code" | ||||
|           :src="qrCodeImage" | ||||
|           mode="aspectFit" | ||||
|           v-if="qrCodeImage" | ||||
|           class="share-bg-image" | ||||
|           src="/static/images/share-bg.jpg" | ||||
|           mode="scaleToFill" | ||||
|           crossorigin="anonymous" | ||||
|           @load="onBackgroundImageLoad" | ||||
|           @error="onBackgroundImageError" | ||||
|         ></image> | ||||
|         <view v-else class="qr-code-placeholder"> | ||||
|           <view class="loader"></view> | ||||
|         <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 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> | ||||
| 
 | ||||
|       <!-- The real, clickable button --> | ||||
|       <button class="share-button" @click="sharePage" v-show="shareButtonShow"> | ||||
|         保存图片并分享 | ||||
|       </button> | ||||
|     </view> | ||||
| 
 | ||||
|     <!-- Canvas for generating the share image, positioned off-screen --> | ||||
|     <canvas | ||||
|       canvas-id="shareCanvas" | ||||
|       :style="{ | ||||
|         width: canvasWidth + 'px', | ||||
|         height: canvasHeight + 'px', | ||||
|         position: 'fixed', | ||||
|         left: '200%', | ||||
|       }" | ||||
|     /> | ||||
|     <cl-tabbar :current="2" /> | ||||
|     <cl-tabbar class="tabbar" :current="2" /> | ||||
|   </view> | ||||
| </template> | ||||
| 
 | ||||
|  | @ -39,6 +72,7 @@ | |||
| import html2canvas from 'html2canvas' | ||||
| import { getShareCode } from '@/config/share' | ||||
| import clTabbar from '@/components/cl-tabbar.vue' | ||||
| 
 | ||||
| export default { | ||||
|   name: 'ShareQRCode', | ||||
|   components: { | ||||
|  | @ -52,16 +86,20 @@ export default { | |||
|       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 | ||||
|         // Set canvas height to a fixed 800px as requested | ||||
|         this.canvasHeight = 800 | ||||
|         this.canvasHeight = res.windowHeight | ||||
|       }, | ||||
|     }) | ||||
|   }, | ||||
|  | @ -72,6 +110,20 @@ export default { | |||
|     }, 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: '加载中...' }) | ||||
|  | @ -80,6 +132,11 @@ export default { | |||
|           // 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: '获取分享码失败', | ||||
|  | @ -104,388 +161,164 @@ export default { | |||
|         }) | ||||
|         return | ||||
|       } | ||||
|       this.shareButtonShow = false | ||||
|       uni.showLoading({ title: '正在生成图片...' }) | ||||
| 
 | ||||
|       // 统一使用html2canvas生成图片 | ||||
|       uni.showLoading({ title: '加载中...' }) | ||||
| 
 | ||||
|       try { | ||||
|         // --- Button Swap Logic --- | ||||
|         // 隐藏按钮,等待DOM更新 | ||||
|         this.shareButtonShow = false | ||||
|         // Wait for DOM to update with the fake button | ||||
|         await this.$nextTick() | ||||
| 
 | ||||
|         // #ifdef H5 | ||||
|         // Capture the entire container as requested | ||||
|         await this.captureWithHtml2Canvas() | ||||
|         // #endif | ||||
| 
 | ||||
|         // #ifndef H5 | ||||
|         // Draw the entire container using Canvas | ||||
|         await this.generateImageWithCanvas() | ||||
|         // #endif | ||||
|         // 使用html2canvas截取页面 | ||||
|         await this.capturePageWithHtml2Canvas() | ||||
|       } catch (error) { | ||||
|         uni.hideLoading() | ||||
|         uni.showToast({ title: '图片生成失败,请稍后重试', icon: 'none' }) | ||||
|         console.error('sharePage error:', error) | ||||
|       } finally { | ||||
|         // --- Always swap back to the real button --- | ||||
|         // 恢复按钮显示 | ||||
|         this.shareButtonShow = true | ||||
|       } | ||||
|     }, | ||||
| 
 | ||||
|     // --- H5-specific method --- | ||||
|     captureWithHtml2Canvas() { | ||||
|       return new Promise((resolve, reject) => { | ||||
|         // #ifdef H5 | ||||
|         // Target the entire #shareContainer as per instruction | ||||
|         const element = document.getElementById('shareContainer') | ||||
|         if (!element) { | ||||
|           uni.hideLoading() | ||||
|           return reject(new Error('Share container element not found')) | ||||
|         } | ||||
|     // 生成的图片加载成功 | ||||
|     onGeneratedImageLoad() { | ||||
|       console.log('生成的图片加载成功') | ||||
|     }, | ||||
| 
 | ||||
|         html2canvas(element, { | ||||
|           useCORS: true, | ||||
|           allowTaint: true, | ||||
|           backgroundColor: null, | ||||
|           scale: window.devicePixelRatio || 2, | ||||
|           logging: false, | ||||
|         }) | ||||
|           .then(canvas => { | ||||
|             this.saveImageToAlbum(canvas.toDataURL('image/png', 1.0)) | ||||
|             resolve() | ||||
|           }) | ||||
|           .catch(err => { | ||||
|             console.error('html2canvas capture failed:', err) | ||||
|             reject(err) | ||||
|           }) | ||||
|         // #endif | ||||
|     // 生成的图片加载失败 | ||||
|     onGeneratedImageError(e) { | ||||
|       console.error('生成的图片加载失败:', e) | ||||
|       uni.showToast({ | ||||
|         title: '图片显示失败', | ||||
|         icon: 'none', | ||||
|       }) | ||||
|     }, | ||||
| 
 | ||||
|     // A robust canvas-based image generation for all platforms | ||||
|     async generateImageWithCanvas() { | ||||
|       try { | ||||
|         // Load all required images before drawing | ||||
|         const [bgImageTempPath, qrCodeTempPath] = await Promise.all([ | ||||
|           this.getLocalImageTempPath('/static/images/share-bg.png'), | ||||
|           this.base64ToTempFilePath(this.qrCodeImage), | ||||
|         ]) | ||||
|     // 关闭全屏图片 | ||||
|     closeFullscreenImage() { | ||||
|       this.generatedImageUrl = '' | ||||
|       console.log('关闭全屏图片') | ||||
|     }, | ||||
| 
 | ||||
|         if (!bgImageTempPath || !qrCodeTempPath) { | ||||
|           throw new Error('Image resource failed to load') | ||||
|         } | ||||
|     // 背景图片加载成功 | ||||
|     onBackgroundImageLoad() { | ||||
|       this.backgroundImageLoaded = true | ||||
|       console.log('背景图片加载成功') | ||||
|     }, | ||||
| 
 | ||||
|         const ctx = uni.createCanvasContext('shareCanvas', this) | ||||
|         // Draw the entire scene to the canvas | ||||
|         this.drawSceneToCanvas(ctx, bgImageTempPath, qrCodeTempPath) | ||||
|     // 背景图片加载失败 | ||||
|     onBackgroundImageError(e) { | ||||
|       console.error('背景图片加载失败:', e) | ||||
|     }, | ||||
| 
 | ||||
|         return new Promise(resolve => { | ||||
|     // 使用html2canvas截取整个页面 | ||||
|     async capturePageWithHtml2Canvas() { | ||||
|       console.log('开始使用html2canvas截取页面') | ||||
| 
 | ||||
|       return new Promise((resolve, reject) => { | ||||
|         // 确保所有图片都已加载完成 | ||||
|         const waitForImages = () => { | ||||
|           if (!this.backgroundImageLoaded || !this.qrCodeImage) { | ||||
|             setTimeout(waitForImages, 100) | ||||
|             return | ||||
|           } | ||||
| 
 | ||||
|           // 额外等待确保渲染完成 | ||||
|           setTimeout(() => { | ||||
|             ctx.draw(false, () => { | ||||
|               setTimeout(() => { | ||||
|                 this.saveCanvasToAlbum() | ||||
|                 resolve() | ||||
|               }, 500) | ||||
|             }) | ||||
|           }, 100) | ||||
|         }) | ||||
|       } catch (error) { | ||||
|         console.error('generateImageWithCanvas error:', error) | ||||
|         throw error | ||||
|       } | ||||
|     }, | ||||
| 
 | ||||
|     // Main drawing function to replicate the entire scene on canvas | ||||
|     drawSceneToCanvas(ctx, bgPath, qrPath) { | ||||
|       const canvasWidth = this.canvasWidth | ||||
|       const canvasHeight = this.canvasHeight // Fixed at 800px | ||||
| 
 | ||||
|       // 1. Draw background image | ||||
|       ctx.drawImage(bgPath, 0, 0, canvasWidth, canvasHeight) | ||||
| 
 | ||||
|       // 2. Draw portal frame (calculations for centering) | ||||
|       const portalWidth = canvasWidth * 0.85 | ||||
|       const portalPadding = 20 | ||||
|       // The white card's width is the portal's inner width | ||||
|       const cardWidth = portalWidth - portalPadding * 2 | ||||
|       // The card is a square, so its height is its width | ||||
|       const cardHeight = cardWidth | ||||
|       const buttonHeight = 50 | ||||
|       const buttonMargin = 20 | ||||
|       const portalHeight = | ||||
|         portalPadding * 2 + cardHeight + buttonMargin + buttonHeight | ||||
|       const portalX = (canvasWidth - portalWidth) / 2 | ||||
|       const portalY = (canvasHeight - portalHeight) / 2 | ||||
|       this.drawPortalFrame(ctx, portalX, portalY, portalWidth, portalHeight) | ||||
| 
 | ||||
|       // 3. Draw the white QR card | ||||
|       const cardX = portalX + portalPadding | ||||
|       const cardY = portalY + portalPadding | ||||
|       this.drawQrCodeCard(ctx, cardX, cardY, cardWidth, cardHeight) | ||||
| 
 | ||||
|       // 4. Draw the FAKE button to match the CSS | ||||
|       const buttonX = cardX | ||||
|       const buttonY = cardY + cardHeight + buttonMargin | ||||
|       this.drawStyledButton( | ||||
|         ctx, | ||||
|         buttonX, | ||||
|         buttonY, | ||||
|         cardWidth, | ||||
|         buttonHeight, | ||||
|         '保存图片并分享' | ||||
|       ) | ||||
| 
 | ||||
|       // 5. Draw the actual QR image (or loader placeholder) on top | ||||
|       // If qrPath is null/empty, this block is skipped, leaving the white card empty (like a placeholder) | ||||
|       if (qrPath) { | ||||
|         // Padding inside the white card | ||||
|         const imagePadding = cardWidth * 0.1 | ||||
|         const qrCodeSize = cardWidth - imagePadding * 2 | ||||
|         const qrCodeX = cardX + imagePadding | ||||
|         const qrCodeY = cardY + imagePadding | ||||
|         ctx.drawImage(qrPath, qrCodeX, qrCodeY, qrCodeSize, qrCodeSize) | ||||
|       } else { | ||||
|         // Draw loader manually if QR is not available | ||||
|         // This part is complex, for now we show an empty card which is better than broken UI | ||||
|       } | ||||
|     }, | ||||
| 
 | ||||
|     // Helper: Draws the portal frame to match CSS | ||||
|     drawPortalFrame(ctx, x, y, width, height) { | ||||
|       const radius = 25 // 50rpx | ||||
|       ctx.save() | ||||
|       this.drawRoundedRect(ctx, x, y, width, height, radius) | ||||
|       // Frosted glass effect | ||||
|       const portalGradient = ctx.createLinearGradient(x, y, x, y + height) | ||||
|       portalGradient.addColorStop(0, 'rgba(255, 255, 255, 0.4)') | ||||
|       portalGradient.addColorStop(1, 'rgba(255, 255, 255, 0.2)') | ||||
|       ctx.fillStyle = portalGradient | ||||
|       ctx.fill() | ||||
|       // Inner glow border | ||||
|       ctx.shadowColor = 'rgba(255, 255, 255, 0.5)' | ||||
|       ctx.shadowBlur = 10 | ||||
|       ctx.strokeStyle = 'rgba(255, 255, 255, 0.6)' | ||||
|       ctx.lineWidth = 1.5 | ||||
|       ctx.stroke() | ||||
|       ctx.restore() | ||||
|     }, | ||||
| 
 | ||||
|     // New/Refactored Helper: Draws the single white QR Code card | ||||
|     drawQrCodeCard(ctx, x, y, width, height) { | ||||
|       const borderRadius = 20 // 40rpx | ||||
| 
 | ||||
|       ctx.save() | ||||
|       this.drawRoundedRect(ctx, x, y, width, height, borderRadius) | ||||
|       // Card background | ||||
|       ctx.fillStyle = 'rgba(255, 255, 255, 0.98)' | ||||
|       // Card shadow | ||||
|       ctx.shadowColor = 'rgba(50, 50, 90, 0.1)' | ||||
|       ctx.shadowBlur = 30 | ||||
|       ctx.shadowOffsetY = 15 | ||||
|       ctx.fill() | ||||
|       // Card border | ||||
|       ctx.strokeStyle = '#f0f0f0' | ||||
|       ctx.lineWidth = 1 | ||||
|       ctx.stroke() | ||||
|       ctx.restore() | ||||
|     }, | ||||
| 
 | ||||
|     // Helper: Draws the styled button | ||||
|     drawStyledButton(ctx, x, y, width, height, text) { | ||||
|       const radius = height / 2 | ||||
| 
 | ||||
|       ctx.save() | ||||
|       this.drawRoundedRect(ctx, x, y, width, height, radius) | ||||
|       // Background gradient | ||||
|       const gradient = ctx.createLinearGradient(x, y, x + width, y) | ||||
|       gradient.addColorStop(0, '#0ff0fc') | ||||
|       gradient.addColorStop(0.5, '#0072ff') | ||||
|       gradient.addColorStop(1, '#00c6ff') | ||||
|       ctx.fillStyle = gradient | ||||
|       // Shadow | ||||
|       ctx.shadowColor = 'rgba(0, 114, 255, 0.35)' | ||||
|       ctx.shadowBlur = 20 | ||||
|       ctx.shadowOffsetY = 8 | ||||
|       ctx.fill() | ||||
|       ctx.restore() | ||||
| 
 | ||||
|       // Text | ||||
|       ctx.save() | ||||
|       ctx.fillStyle = '#ffffff' | ||||
|       ctx.font = '17px sans-serif' | ||||
|       ctx.textAlign = 'center' | ||||
|       ctx.textBaseline = 'middle' | ||||
|       ctx.fillText(text, x + width / 2, y + height / 2) | ||||
|       ctx.restore() | ||||
|     }, | ||||
| 
 | ||||
|     // Helper: Draw a rectangle with rounded corners | ||||
|     drawRoundedRect(ctx, x, y, width, height, radius) { | ||||
|       if (width < 2 * radius) radius = width / 2 | ||||
|       if (height < 2 * radius) radius = height / 2 | ||||
|       ctx.beginPath() | ||||
|       ctx.moveTo(x + radius, y) | ||||
|       ctx.arcTo(x + width, y, x + width, y + height, radius) | ||||
|       ctx.arcTo(x + width, y + height, x, y + height, radius) | ||||
|       ctx.arcTo(x, y + height, x, y, radius) | ||||
|       ctx.arcTo(x, y, x + width, y, radius) | ||||
|       ctx.closePath() | ||||
|     }, | ||||
| 
 | ||||
|     saveCanvasToAlbum() { | ||||
|       try { | ||||
|         // 统一的canvas转换参数 | ||||
|         const options = { | ||||
|           canvasId: 'shareCanvas', | ||||
|           fileType: 'png', | ||||
|           quality: 1, | ||||
|           success: res => { | ||||
|             console.log('canvas转换成功:', res) | ||||
|             if (res.tempFilePath) { | ||||
|               this.saveImageToAlbum(res.tempFilePath) | ||||
|             } else { | ||||
|               uni.hideLoading() | ||||
|               uni.showToast({ title: '图片生成失败', icon: 'none' }) | ||||
|             const element = document.getElementById('shareContainer') | ||||
|             if (!element) { | ||||
|               reject(new Error('找不到页面容器')) | ||||
|               return | ||||
|             } | ||||
|           }, | ||||
|           fail: err => { | ||||
|             console.error('canvas转换失败:', err) | ||||
|             uni.hideLoading() | ||||
|             uni.showToast({ title: '图片转换失败', icon: 'none' }) | ||||
|           }, | ||||
| 
 | ||||
|             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 | ||||
|         } | ||||
| 
 | ||||
|         // 调用canvas转换API | ||||
|         uni.canvasToTempFilePath(options, this) | ||||
|       } catch (error) { | ||||
|         console.error('saveCanvasToAlbum error:', error) | ||||
|         uni.hideLoading() | ||||
|         uni.showToast({ title: '保存失败', icon: 'none' }) | ||||
|       } | ||||
|         // 开始等待图片加载 | ||||
|         waitForImages() | ||||
|       }) | ||||
|     }, | ||||
| 
 | ||||
|     // 新增:统一的图片保存方法 | ||||
|     saveImageToAlbum(filePath) { | ||||
|       // #ifdef H5 | ||||
|       // For H5, trigger download instead of saving to album | ||||
|     // 原生图片加载器 | ||||
|     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 = filePath | ||||
|       link.href = dataUrl | ||||
|       link.download = `share_page_${Date.now()}.png` | ||||
|       document.body.appendChild(link) | ||||
|       link.click() | ||||
|       document.body.removeChild(link) | ||||
|       uni.hideLoading() | ||||
|       uni.showToast({ | ||||
|         title: '图片已开始下载', | ||||
|         icon: 'success', | ||||
|       }) | ||||
|       // #endif | ||||
| 
 | ||||
|       // #ifndef H5 | ||||
|       // For App and Mini Programs | ||||
|       uni.saveImageToPhotosAlbum({ | ||||
|         filePath: filePath, | ||||
|         success: () => { | ||||
|           uni.hideLoading() | ||||
|           uni.showToast({ | ||||
|             title: '图片已保存到相册', | ||||
|             icon: 'success', | ||||
|           }) | ||||
|         }, | ||||
|         fail: err => { | ||||
|           uni.hideLoading() | ||||
|           if ( | ||||
|             err.errMsg && | ||||
|             (err.errMsg.includes('auth deny') || | ||||
|               err.errMsg.includes('auth denied')) | ||||
|           ) { | ||||
|             uni.showModal({ | ||||
|               title: '提示', | ||||
|               content: '需要您授权保存相册', | ||||
|               showCancel: false, | ||||
|               success: () => { | ||||
|                 uni.openSetting({ | ||||
|                   success(settingdata) { | ||||
|                     if (settingdata.authSetting['scope.writePhotosAlbum']) { | ||||
|                       uni.showToast({ | ||||
|                         title: '授权成功,请重试', | ||||
|                         icon: 'none', | ||||
|                       }) | ||||
|                     } else { | ||||
|                       uni.showToast({ | ||||
|                         title: '获取权限失败', | ||||
|                         icon: 'none', | ||||
|                       }) | ||||
|                     } | ||||
|                   }, | ||||
|                 }) | ||||
|               }, | ||||
|             }) | ||||
|           } else { | ||||
|             uni.showToast({ title: '保存失败', icon: 'none' }) | ||||
|             console.error('saveImageToPhotosAlbum fail:', err) | ||||
|           } | ||||
|         }, | ||||
|       }) | ||||
|       // #endif | ||||
|     }, | ||||
| 
 | ||||
|     base64ToTempFilePath(base64) { | ||||
|       return new Promise((resolve, reject) => { | ||||
|         try { | ||||
|           // #ifdef H5 | ||||
|           // For H5, we load the base64 into an Image to ensure it's valid, | ||||
|           // but resolve with the base64 string to avoid Uniapp's internal errors | ||||
|           // when its functions expect a string path instead of an Image object. | ||||
|           const image = new Image() | ||||
|           // Resolve CORS issue for QR code from different origin | ||||
|           image.crossOrigin = 'Anonymous' | ||||
|           image.src = base64 | ||||
|           image.onload = () => { | ||||
|             // Resolve with the string, not the object. | ||||
|             resolve(base64) | ||||
|           } | ||||
|           image.onerror = err => { | ||||
|             console.error('Failed to load image for canvas on H5', err) | ||||
|             reject(new Error('H5图片加载失败')) | ||||
|           } | ||||
|           // #endif | ||||
| 
 | ||||
|           // #ifndef H5 | ||||
|           // For App and Mini Programs, write to a temp file and return the path. | ||||
|           if (!base64 || typeof base64 !== 'string') { | ||||
|             reject(new Error('无效的base64数据')) | ||||
|             return | ||||
|           } | ||||
| 
 | ||||
|           const formattedBase64 = base64.replace(/^data:image\/\w+;base64,/, '') | ||||
|           if (!formattedBase64) { | ||||
|             reject(new Error('base64数据格式错误')) | ||||
|             return | ||||
|           } | ||||
| 
 | ||||
|           // Use a standard path for user data directory. | ||||
|           const filePath = `${uni.env.USER_DATA_PATH}/share_${Date.now()}.png` | ||||
|           const fileManager = uni.getFileSystemManager() | ||||
| 
 | ||||
|           fileManager.writeFile({ | ||||
|             filePath, | ||||
|             data: formattedBase64, | ||||
|             encoding: 'base64', | ||||
|             success: () => { | ||||
|               resolve(filePath) | ||||
|             }, | ||||
|             fail: err => { | ||||
|               console.error('Failed to write temp file', err) | ||||
|               reject(new Error('临时文件写入失败')) | ||||
|             }, | ||||
|           }) | ||||
|           // #endif | ||||
|         } catch (error) { | ||||
|           console.error('base64ToTempFilePath error:', error) | ||||
|           reject(new Error('图片处理失败')) | ||||
|         } | ||||
|       }) | ||||
|     }, | ||||
|   }, | ||||
| } | ||||
|  | @ -493,9 +326,8 @@ export default { | |||
| 
 | ||||
| <style lang="scss" scoped> | ||||
| .share-container { | ||||
|   background: url('@/static/images/share-bg.png') no-repeat center center; | ||||
|   background-size: 100% 100%; | ||||
|   height: calc(100vh - 80rpx); | ||||
|   flex: 1; | ||||
|   height: 0; | ||||
|   box-sizing: border-box; | ||||
|   position: relative; | ||||
|   overflow: hidden; | ||||
|  | @ -503,6 +335,17 @@ export default { | |||
|   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%; | ||||
|  | @ -511,26 +354,30 @@ export default { | |||
|     height: 100%; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| .portal-frame { | ||||
|   /* Restyled to match the ideal screenshot */ | ||||
|   background: rgba(255, 255, 255, 0.25); | ||||
|   backdrop-filter: blur(20px); | ||||
|   -webkit-backdrop-filter: blur(20px); | ||||
|   padding: 40rpx; | ||||
|   margin-top: 450rpx; | ||||
|   border-radius: 50rpx; | ||||
|   border: 1.5px solid rgba(255, 255, 255, 0.6); | ||||
|   box-shadow: | ||||
|     0 0 0 1.5px rgba(255, 255, 255, 0.4) inset, | ||||
|     0 15rpx 40rpx rgba(0, 0, 0, 0.1); | ||||
| .share-wrapper { | ||||
|   position: absolute; | ||||
|   z-index: 2; | ||||
|   bottom: 260rpx; | ||||
|   display: flex; | ||||
|   width: 90%; | ||||
|   box-sizing: border-box; | ||||
| 
 | ||||
|   flex-direction: column; | ||||
|   align-items: center; | ||||
|   /* Removed margin-top, parent container handles centering */ | ||||
| } | ||||
| .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 { | ||||
|  | @ -547,17 +394,16 @@ export default { | |||
| 
 | ||||
| /* The single white card for the QR code */ | ||||
| .qr-code-outer { | ||||
|   width: 100%; | ||||
|   /* This creates a responsive square that's always perfect */ | ||||
|   aspect-ratio: 1 / 1; | ||||
|   width: 400rpx; /* 从400rpx缩小到320rpx */ | ||||
|   height: 400rpx; /* 从400rpx缩小到320rpx */ | ||||
|   background: rgba(255, 255, 255, 0.98); | ||||
|   border-radius: 40rpx; | ||||
|   box-shadow: 0px 15rpx 30rpx rgba(50, 50, 90, 0.1); | ||||
|   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: 30rpx; /* Padding inside the white card */ | ||||
|   padding: 12rpx; /* 从16rpx减小到12rpx */ | ||||
|   box-sizing: border-box; | ||||
| } | ||||
| 
 | ||||
|  | @ -600,31 +446,91 @@ export default { | |||
| 
 | ||||
| /* Unified style for both the real button and the fake one (which is a view) */ | ||||
| .share-button { | ||||
|   margin-top: 40rpx; | ||||
|   width: 100%; /* Button takes full width of portal frame padding */ | ||||
|   height: 100rpx; | ||||
|   line-height: 100rpx; | ||||
|   margin-top: 28rpx; /* 从32rpx减小到28rpx */ | ||||
|   width: 280rpx; /* 设置固定宽度 */ | ||||
|   height: 72rpx; /* 从88rpx减小到72rpx */ | ||||
|   line-height: 72rpx; | ||||
|   color: #fff; | ||||
|   border-radius: 50rpx; | ||||
|   font-size: 34rpx; | ||||
|   /* Match the gradient from canvas drawing */ | ||||
|   background: linear-gradient(100deg, #0ff0fc 0%, #0072ff 50%, #00c6ff 100%); | ||||
|   box-shadow: 0 8rpx 20rpx 0 rgba(0, 114, 255, 0.35); | ||||
|   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; | ||||
|   /* Add transition for the button itself if needed */ | ||||
|   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: 100rpx; /* Ensure text is centered vertically */ | ||||
|   line-height: 72rpx; /* 更新行高到72rpx */ | ||||
|   border: none; | ||||
| } | ||||
| 
 | ||||
| .share-button:active { | ||||
|   transform: translateY(2rpx); | ||||
|   box-shadow: 0 6rpx 12rpx rgba(0, 114, 255, 0.3); | ||||
|   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> | ||||
|  |  | |||
										
											Binary file not shown.
										
									
								
							| Before Width: | Height: | Size: 372 KiB After Width: | Height: | Size: 985 KiB | 
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 26 KiB | 
		Loading…
	
		Reference in New Issue