3
0
Fork 0
web-store-retail-h5/pages/mine/share/index.vue

457 lines
12 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<view class="share-container">
<!-- This is the content that will be shared as an image -->
<view class="share-content" :class="{ 'is-loaded': isLoaded }">
<view class="title">扫码注册</view>
<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 class="tip">扫描二维码即可完成操作</view>
</view>
<button
class="share-button"
:class="{ 'is-loaded': isLoaded }"
@click="sharePage"
>
保存图片并分享
</button>
<!-- Canvas for generating the share image, positioned off-screen -->
<canvas
canvas-id="shareCanvas"
:style="{
width: canvasWidth + 'px',
height: canvasHeight + 'px',
position: 'fixed',
left: '200%',
}"
/>
</view>
</template>
<script>
import { getShareCode } from '@/config/share'
export default {
name: 'ShareQRCode',
data() {
return {
qrCodeImage: '',
// Set canvas dimensions. It's better to get device screen width for this.
canvasWidth: 375,
canvasHeight: 550,
isLoaded: false,
}
},
onLoad() {
this.handleGetShareCode()
// Get screen width to set canvas width dynamically
uni.getSystemInfo({
success: res => {
this.canvasWidth = res.windowWidth
// Adjust height proportionally or keep it fixed
this.canvasHeight = res.windowWidth * 1.4
},
})
},
onReady() {
// Use a short timeout to ensure the initial render is complete before animation
setTimeout(() => {
this.isLoaded = true
}, 100)
},
methods: {
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
} 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
}
uni.showLoading({ title: '正在生成图片...' })
try {
const tempImagePath = await this.base64ToTempFilePath(this.qrCodeImage)
if (!tempImagePath) {
throw new Error('图片处理失败')
}
const ctx = uni.createCanvasContext('shareCanvas', this)
this.drawShareImage(ctx, tempImagePath)
ctx.draw(false, () => {
this.saveCanvasToAlbum()
})
} catch (error) {
uni.hideLoading()
uni.showToast({ title: error.message || '图片生成失败', icon: 'none' })
console.error('sharePage error:', error)
}
},
drawShareImage(ctx, tempImagePath) {
const canvasWidth = this.canvasWidth
const canvasHeight = this.canvasHeight
// White background
ctx.fillStyle = '#FFFFFF'
ctx.fillRect(0, 0, canvasWidth, canvasHeight)
// Title
ctx.setFontSize(22)
ctx.fillStyle = '#1e1e1e'
ctx.textAlign = 'center'
ctx.fillText('扫码注册', canvasWidth / 2, 70)
// QR Code Image
const qrCodeSize = canvasWidth * 0.7
const qrCodeX = (canvasWidth - qrCodeSize) / 2
const qrCodeY = 120
ctx.drawImage(tempImagePath, qrCodeX, qrCodeY, qrCodeSize, qrCodeSize)
// Tip text
ctx.setFontSize(15)
ctx.fillStyle = '#888'
ctx.textAlign = 'center'
ctx.fillText(
'扫描二维码,即可完成操作',
canvasWidth / 2,
qrCodeY + qrCodeSize + 50
)
},
saveCanvasToAlbum() {
uni.canvasToTempFilePath(
{
canvasId: 'shareCanvas',
success: res => {
// #ifdef H5
// For H5, trigger download instead of saving to album
const link = document.createElement('a')
link.href = res.tempFilePath
link.download = `share_qrcode_${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: res.tempFilePath,
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
},
fail: err => {
uni.hideLoading()
uni.showToast({ title: '图片转换失败', icon: 'none' })
console.error('canvasToTempFilePath fail:', err)
},
},
this
)
},
base64ToTempFilePath(base64) {
return new Promise((resolve, reject) => {
// #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.
const formattedBase64 = base64.replace(/^data:image\/\w+;base64,/, '')
// Use a standard path for user data directory.
const filePath = `${uni.env.USER_DATA_PATH}/share_${Date.now()}.png`
uni.getFileSystemManager().writeFile({
filePath,
data: formattedBase64,
encoding: 'base64',
success: () => {
resolve(filePath)
},
fail: err => {
console.error('Failed to write temp file', err)
reject(new Error('临时文件写入失败'))
},
})
// #endif
})
},
},
}
</script>
<style lang="scss" scoped>
.share-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40rpx;
background: linear-gradient(to bottom, #e0f7fa 0%, #ffffff 100%);
min-height: 100vh;
box-sizing: border-box;
position: relative;
overflow: hidden;
}
@keyframes float {
0% {
transform: translateY(0px) scale(1);
opacity: 0.7;
}
50% {
transform: translateY(-20px) scale(1.03);
opacity: 1;
}
100% {
transform: translateY(0px) scale(1);
opacity: 0.7;
}
}
.share-container::before,
.share-container::after {
content: '';
position: absolute;
border-radius: 50%;
background: linear-gradient(
to top,
rgba(0, 198, 255, 0.05),
rgba(0, 114, 255, 0.1)
);
z-index: 1;
pointer-events: none;
}
.share-container::before {
width: 400rpx;
height: 400rpx;
top: -150rpx;
left: -150rpx;
animation: float 12s ease-in-out infinite;
}
.share-container::after {
width: 500rpx;
height: 500rpx;
bottom: -200rpx;
right: -200rpx;
animation: float 15s ease-in-out infinite -5s;
}
.share-content {
background: radial-gradient(
circle at 50% 0%,
rgba(220, 235, 255, 0.9),
#ffffff 80%
);
border-radius: 30rpx;
padding: 80rpx 50rpx;
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
box-shadow:
0 16rpx 48rpx rgba(0, 0, 0, 0.1),
inset 0 1px 2px rgba(255, 255, 255, 0.7);
margin-bottom: 60rpx;
position: relative;
z-index: 2;
opacity: 0;
transform: translateY(40rpx);
transition:
transform 0.6s cubic-bezier(0.25, 1, 0.5, 1),
opacity 0.6s ease;
overflow: hidden;
}
.share-content::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 6rpx;
background-image: linear-gradient(90deg, #00c6ff, #0072ff);
opacity: 0.9;
}
.share-content.is-loaded {
opacity: 1;
transform: translateY(0);
}
.title {
font-size: 44rpx;
font-weight: 500;
color: #1e1e1e;
margin-bottom: 60rpx;
}
.qr-code {
width: 450rpx;
height: 450rpx;
margin-bottom: 30rpx;
border-radius: 16rpx;
}
.qr-code-placeholder {
width: 450rpx;
height: 450rpx;
background-color: #f0f2f5;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 30rpx;
border-radius: 16rpx;
}
.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;
}
.share-button {
margin-top: 0;
width: 90%;
background-image: linear-gradient(90deg, #0072ff, #00c6ff);
color: white;
border-radius: 50rpx;
font-size: 34rpx;
height: 100rpx;
line-height: 100rpx;
box-shadow: 0 10rpx 20rpx rgba(0, 114, 255, 0.25);
border: none;
transition:
transform 0.2s ease,
box-shadow 0.2s ease;
z-index: 2;
opacity: 0;
transform: translateY(40rpx);
transition:
transform 0.6s cubic-bezier(0.25, 1, 0.5, 1) 0.1s,
opacity 0.6s ease 0.1s;
}
.share-button.is-loaded {
opacity: 1;
transform: translateY(0);
}
.share-button:active {
transform: translateY(2rpx);
box-shadow: 0 6rpx 12rpx rgba(0, 114, 255, 0.3);
}
</style>