631 lines
16 KiB
Vue
631 lines
16 KiB
Vue
<template>
|
||
<view style="font-family: YouSheBiaoTiHei;">
|
||
<wd-popup custom-class="my-custom-popup" v-model="innerShow" @close="onClose">
|
||
<view class="share-poster">
|
||
<!-- 海报画布 -->
|
||
<canvas class="poster-canvas" canvas-id="posterCanvas"
|
||
:style="{ width: posterWidth + 'px', height: posterHeight + 'px' }"></canvas>
|
||
</view>
|
||
<!-- 分享操作按钮 -->
|
||
<view class="share-actions">
|
||
<view class="action-item left" @click="shareToFriend"> 微信好友 </view>
|
||
<view class="action-item right" @click="savePoster"> 保存相册 </view>
|
||
</view>
|
||
</wd-popup>
|
||
</view>
|
||
</template>
|
||
|
||
<script>
|
||
/**
|
||
* @property {Object} posterInfo - 商品信息
|
||
* @property {string} posterInfo.title - 商品标题
|
||
* @property {number} posterInfo.price - 商品价格
|
||
* @property {string} posterInfo.image - 商品主图URL
|
||
* @property {string} posterInfo.qrcode - 小程序码URL
|
||
* @property {boolean} show - 控制弹窗显示隐藏
|
||
* @event updateShare - 弹窗关闭时触发,参数为false
|
||
*/
|
||
export default {
|
||
name: 'SharePoster',
|
||
props: {
|
||
posterInfo: {
|
||
type: Object,
|
||
required: true,
|
||
default: () => ({
|
||
title: '',
|
||
name: '',
|
||
qrcode: '',
|
||
backImage: ''
|
||
}),
|
||
},
|
||
show: {
|
||
type: Boolean,
|
||
default: false,
|
||
},
|
||
},
|
||
data() {
|
||
return {
|
||
innerShow: false, // 内部控制弹窗显示状态
|
||
generating: false, // 海报生成状态
|
||
posterWidth: 300, // 减小画布宽度
|
||
posterHeight: 450, // 减小画布高度
|
||
loadingText: {
|
||
contentdown: '生成海报中...',
|
||
contentrefresh: '生成海报中...',
|
||
contentnomore: '生成完成',
|
||
},
|
||
imgUrl: '',
|
||
}
|
||
},
|
||
watch: {
|
||
// 监听show变化,显示时生成海报
|
||
show(newVal) {
|
||
this.innerShow = newVal;
|
||
// if (newVal) {
|
||
this.$nextTick(() => {
|
||
console.log('Show changed, generating poster')
|
||
let self = this
|
||
self.generatePoster()
|
||
|
||
// uni.loadFontFace({
|
||
// family: 'YouSheBiaoTiHei',
|
||
// source: 'url("https://hnxix-public.oss-cn-hangzhou.aliyuncs.com/driver/wx/font/YouSheBiaoTiHei.ttf")',
|
||
// success() {
|
||
// console.log('success')
|
||
// self.generatePoster()
|
||
// },
|
||
// scopes: ["webview", "native"],
|
||
// })
|
||
})
|
||
// }
|
||
},
|
||
posterInfo: {
|
||
immediate: true,
|
||
deep: true,
|
||
handler(newVal) {
|
||
console.log('SharePoster posterInfo changed:', newVal)
|
||
},
|
||
},
|
||
},
|
||
methods: {
|
||
/**
|
||
* 关闭弹窗
|
||
* 触发updateShare事件,参数为false
|
||
*/
|
||
onClose() {
|
||
// 销毁画布内容
|
||
const ctx = uni.createCanvasContext('posterCanvas', this)
|
||
ctx.clearRect(0, 0, this.posterWidth, this.posterHeight)
|
||
ctx.draw() // 清空画布
|
||
this.generating = false // 重置状态
|
||
this.$emit('updateShare', false)
|
||
},
|
||
|
||
/**
|
||
* 生成分享海报
|
||
* 步骤:
|
||
* 1. 创建画布上下文
|
||
* 2. 绘制白色背景
|
||
* 3. 绘制商品图片(圆角)
|
||
* 4. 绘制商品信息
|
||
* 5. 绘制价格
|
||
* 6. 绘制商品类型标签(带圆角)
|
||
* 7. 绘制小程序码(带圆角)
|
||
* 8. 绘制提示文字
|
||
* 9. 刷新画布显示
|
||
*/
|
||
async generatePoster() {
|
||
if (this.generating) return
|
||
this.generating = true
|
||
try {
|
||
const ctx = uni.createCanvasContext('posterCanvas', this)
|
||
// 清除画布
|
||
ctx.clearRect(0, 0, this.posterWidth, this.posterHeight)
|
||
|
||
// 首先绘制纯色背景,确保没有透明区域
|
||
ctx.fillStyle = '#ffffff'
|
||
ctx.fillRect(0, 0, this.posterWidth, this.posterHeight)
|
||
// 绘制背景图片 - 只填充上半部分
|
||
const imageUrl = this.posterInfo.backImage || ''
|
||
if (imageUrl) {
|
||
// 只在上半部分绘制背景图
|
||
await this.drawImage(ctx, imageUrl, 0, 0, this.posterWidth, this.posterHeight)
|
||
}
|
||
|
||
|
||
// 添加白色半透明遮罩作为内容背景
|
||
// ctx.fillStyle = 'rgba(255, 255, 255, 0.85)'
|
||
// ctx.fillRect(20, 80, this.posterWidth - 40, this.posterHeight - 100)
|
||
// 绘制logo - 放在d顶部位置
|
||
|
||
// 绘制标题和品牌
|
||
const gradient = ctx.createLinearGradient(22, 125, 80, 125)
|
||
gradient.addColorStop(0, '#1068FF') // 底部开始
|
||
gradient.addColorStop(0.48, '#2B497E') // 48%位置
|
||
gradient.addColorStop(1, '#D51BB6') // 顶部结束
|
||
|
||
const gradient_sec = ctx.createLinearGradient(22, 160, 130, 160)
|
||
gradient_sec.addColorStop(0, '#1068FF') // 底部开始
|
||
gradient_sec.addColorStop(0.48, '#2B497E') // 48%位置
|
||
gradient_sec.addColorStop(1, '#D51BB6') // 顶部结束
|
||
|
||
ctx.shadowColor = 'rgba(0, 0, 0, 0.15)'
|
||
ctx.shadowBlur = 2
|
||
ctx.shadowOffsetX = 1
|
||
ctx.shadowOffsetY = 1
|
||
|
||
|
||
ctx.font = 'italic bold 20px sans-serif'
|
||
ctx.setFontSize(15)
|
||
ctx.setTextAlign('left')
|
||
ctx.setFillStyle('#0060C4')
|
||
let title = this.posterInfo.title || ''
|
||
ctx.fillText(`${title}`, 18, 35)
|
||
|
||
|
||
ctx.font = 'italic bold 20px sans-serif'
|
||
// ctx.font = "20px bold 'YouSheBiaoTiHei'"
|
||
ctx.setFontSize(16)
|
||
ctx.setTextAlign('left')
|
||
ctx.setFillStyle('#21CCF9')
|
||
let first = this.posterInfo.name || ''
|
||
ctx.fillText(`${first}`, 18, 136)
|
||
|
||
|
||
// 绘制副标题
|
||
ctx.font = 'italic bold 20px sans-serif'
|
||
// ctx.font = "20px bold 'YouSheBiaoTiHei'"
|
||
ctx.setFontSize(16)
|
||
ctx.setTextAlign('left')
|
||
ctx.setFillStyle('#21CCF9')
|
||
ctx.fillText('邀请您入驻小区', 18, 170)
|
||
|
||
|
||
// 重置阴影效果
|
||
ctx.shadowColor = 'transparent'
|
||
ctx.shadowBlur = 0
|
||
ctx.shadowOffsetX = 0
|
||
ctx.shadowOffsetY = 0
|
||
|
||
const qrcodeUrl = this.posterInfo.qrcode || ''
|
||
if (qrcodeUrl) {
|
||
const qrSize = 150
|
||
const qrX = (this.posterWidth - qrSize) / 2
|
||
const qrY = 240 // 放在中间位置
|
||
await this.drawImage(ctx, qrcodeUrl, qrX, qrY, qrSize, qrSize)
|
||
}
|
||
|
||
// 绘制提示文字 - 放在二维码下方
|
||
ctx.setFontSize(11)
|
||
ctx.setFillStyle('#666666')
|
||
ctx.setTextAlign('center')
|
||
ctx.fillText('微信扫码入驻,获取更多特权', this.posterWidth / 2, 420)
|
||
|
||
ctx.draw(false, () => {
|
||
setTimeout(() => {
|
||
this.generating = false
|
||
}, 500)
|
||
})
|
||
} catch (error) {
|
||
console.error('生成海报失败:', error)
|
||
this.generating = false
|
||
uni.showToast({
|
||
title: '生成海报失败',
|
||
icon: 'none',
|
||
})
|
||
}
|
||
},
|
||
|
||
/**
|
||
* 计算等比例缩放后的尺寸和位置
|
||
* @param {number} imgWidth - 原图宽度
|
||
* @param {number} imgHeight - 原图高度
|
||
* @param {number} maxWidth - 容器最大宽度
|
||
* @param {number} maxHeight - 容器最大高度
|
||
* @returns {Object} - 返回计算后的位置和尺寸
|
||
*/
|
||
calculateImageRect(imgWidth, imgHeight, maxWidth, maxHeight) {
|
||
const imgRatio = imgWidth / imgHeight
|
||
const maxRatio = maxWidth / maxHeight
|
||
let finalWidth = maxWidth
|
||
let finalHeight = maxHeight
|
||
let x = 0
|
||
let y = 0
|
||
|
||
// 修改为"覆盖"模式,确保图片填满整个区域
|
||
if (imgRatio > maxRatio) {
|
||
// 图片更宽,以高度为准
|
||
finalHeight = maxHeight
|
||
finalWidth = finalHeight * imgRatio
|
||
x = (maxWidth - finalWidth) / 2
|
||
} else {
|
||
// 图片更高,以宽度为准
|
||
finalWidth = maxWidth
|
||
finalHeight = finalWidth / imgRatio
|
||
y = (maxHeight - finalHeight) / 2
|
||
}
|
||
return {
|
||
x: Math.floor(x),
|
||
y: Math.floor(y),
|
||
width: Math.floor(finalWidth),
|
||
height: Math.floor(finalHeight),
|
||
}
|
||
},
|
||
/**
|
||
* 加载并绘制图片
|
||
* @param {CanvasContext} ctx - 画布上下文
|
||
* @param {string} src - 图片URL
|
||
* @param {number} x - 绘制位置x坐标
|
||
* @param {number} y - 绘制位置y坐标
|
||
* @param {number} width - 容器宽度
|
||
* @param {number} height - 容器高度
|
||
* @returns {Promise} 图片加载完成的Promise
|
||
*/
|
||
drawImage(ctx, src, x, y, width, height) {
|
||
return new Promise((resolve, reject) => {
|
||
uni.getImageInfo({
|
||
src: src,
|
||
success: (image) => {
|
||
// 对于背景图使用填充模式,对于其他元素使用原来的计算方法
|
||
if (x === 0 && y === 0 && width === this.posterWidth && height === this
|
||
.posterHeight) {
|
||
// 背景图片,使用覆盖模式
|
||
const rect = this.calculateImageRect(image.width, image.height, width,
|
||
height)
|
||
ctx.drawImage(image.path, x + rect.x, y + rect.y, rect.width, rect
|
||
.height)
|
||
} else {
|
||
// 其他元素(如二维码等),保持原样绘制
|
||
ctx.drawImage(image.path, x, y, width, height)
|
||
}
|
||
resolve()
|
||
},
|
||
fail: (err) => {
|
||
console.error('加载图片失败:', err)
|
||
reject(err)
|
||
},
|
||
})
|
||
})
|
||
},
|
||
/**
|
||
* 绘制圆角矩形
|
||
* @param {CanvasContext} ctx - 画布上下文
|
||
* @param {number} x - 左上角x坐标
|
||
* @param {number} y - 左上角y坐标
|
||
* @param {number} w - 宽度
|
||
* @param {number} h - 高度
|
||
* @param {number} r - 圆角半径
|
||
*/
|
||
roundRect(ctx, x, y, w, h, r) {
|
||
if (w < 2 * r) r = w / 2
|
||
if (h < 2 * r) r = h / 2
|
||
ctx.beginPath()
|
||
ctx.moveTo(x + r, y)
|
||
ctx.arcTo(x + w, y, x + w, y + h, r)
|
||
ctx.arcTo(x + w, y + h, x, y + h, r)
|
||
ctx.arcTo(x, y + h, x, y, r)
|
||
ctx.arcTo(x, y, x + w, y, r)
|
||
ctx.closePath()
|
||
},
|
||
|
||
/**
|
||
* 文字自动换行
|
||
* @param {CanvasContext} ctx - 画布上下文
|
||
* @param {string} text - 文本内容
|
||
* @param {number} maxWidth - 最大宽度
|
||
* @returns {Array<string>} 文本行数组
|
||
*/
|
||
splitText(ctx, text, maxWidth) {
|
||
const chars = text.split('')
|
||
const lines = []
|
||
let tempLine = ''
|
||
|
||
chars.forEach((char) => {
|
||
if (ctx.measureText(tempLine + char).width <= maxWidth) {
|
||
tempLine += char
|
||
} else {
|
||
lines.push(tempLine)
|
||
tempLine = char
|
||
}
|
||
})
|
||
|
||
if (tempLine) {
|
||
lines.push(tempLine)
|
||
}
|
||
|
||
return lines
|
||
},
|
||
/**
|
||
* 分享给好友
|
||
*/
|
||
shareToFriend() {
|
||
uni.canvasToTempFilePath({
|
||
canvasId: 'posterCanvas',
|
||
success: (res) => {
|
||
console.log(res, '000')
|
||
|
||
// APP环境下,使用uni.share实现分享到微信
|
||
// #ifdef APP-PLUS
|
||
uni.share({
|
||
provider: 'weixin',
|
||
scene: 'WXSceneSession', // WXSceneSession = 好友,WXSceneTimeline = 朋友圈
|
||
type: 2,
|
||
imageUrl: res.tempFilePath,
|
||
success: function() {
|
||
console.log('分享图片成功')
|
||
},
|
||
fail: function(err) {
|
||
console.log('分享图片失败:', err)
|
||
// 提示用户
|
||
uni.showToast({
|
||
title: '分享失败',
|
||
icon: 'none',
|
||
})
|
||
},
|
||
})
|
||
// #endif
|
||
|
||
// 小程序环境下使用showShareImageMenu
|
||
// #ifdef MP-WEIXIN
|
||
uni.showShareImageMenu({
|
||
path: res.tempFilePath,
|
||
success: function() {
|
||
console.log('分享图片成功')
|
||
},
|
||
fail: function(err) {
|
||
console.log('分享图片失败:', err)
|
||
},
|
||
})
|
||
// #endif
|
||
},
|
||
},
|
||
this
|
||
)
|
||
},
|
||
|
||
/**
|
||
* 分享到朋友圈
|
||
*/
|
||
// shareToTimeline() {
|
||
// uni.canvasToTempFilePath({
|
||
// canvasId: 'posterCanvas',
|
||
// success: (res) => {
|
||
// // APP环境下,使用uni.share实现分享到朋友圈
|
||
// // #ifdef APP-PLUS
|
||
// uni.share({
|
||
// provider: 'weixin',
|
||
// scene: 'WXSceneTimeline', // 分享到朋友圈
|
||
// type: 'image',
|
||
// imageUrl: res.tempFilePath,
|
||
// success: function() {
|
||
// console.log('分享到朋友圈成功')
|
||
// },
|
||
// fail: function(err) {
|
||
// console.log('分享到朋友圈失败:', err)
|
||
// // 提示用户
|
||
// uni.showToast({
|
||
// title: '分享失败',
|
||
// icon: 'none'
|
||
// })
|
||
// }
|
||
// })
|
||
// // #endif
|
||
|
||
// // 小程序环境下提示用户
|
||
// // #ifdef MP-WEIXIN
|
||
// uni.showToast({
|
||
// title: '小程序环境不支持直接分享到朋友圈,请保存图片后手动分享',
|
||
// icon: 'none'
|
||
// })
|
||
// // #endif
|
||
// },
|
||
// },
|
||
// this
|
||
// )
|
||
// },
|
||
|
||
/**
|
||
* 保存到本地
|
||
*/
|
||
savePoster() {
|
||
// #ifdef MP-WEIXIN
|
||
// 微信小程序需要检查权限
|
||
this.checkAndRequestPermission()
|
||
// #endif
|
||
|
||
// #ifdef APP-PLUS || MP-ALIPAY || MP-BAIDU || MP-TOUTIAO
|
||
// 其他平台直接保存
|
||
this.saveImageToAlbum()
|
||
// #endif
|
||
|
||
// #ifdef H5
|
||
// H5环境下的处理
|
||
uni.showToast({
|
||
title: 'H5环境不支持保存到相册',
|
||
icon: 'none',
|
||
})
|
||
// #endif
|
||
},
|
||
|
||
/**
|
||
* 检查并请求权限
|
||
*/
|
||
checkAndRequestPermission() {
|
||
uni.getSetting({
|
||
success: (res) => {
|
||
if (!res.authSetting['scope.writePhotosAlbum']) {
|
||
uni.authorize({
|
||
scope: 'scope.writePhotosAlbum',
|
||
success: () => {
|
||
this.saveImageToAlbum()
|
||
},
|
||
fail: () => {
|
||
this.showAuthModal()
|
||
},
|
||
})
|
||
} else {
|
||
this.saveImageToAlbum()
|
||
}
|
||
},
|
||
fail: (err) => {
|
||
console.log('获取设置失败:', err)
|
||
uni.showToast({
|
||
title: '获取权限失败',
|
||
icon: 'none',
|
||
})
|
||
},
|
||
})
|
||
},
|
||
|
||
/**
|
||
* 显示授权引导弹窗
|
||
*/
|
||
|
||
showAuthModal() {
|
||
uni.showModal({
|
||
title: '提示',
|
||
content: '需要您授权保存图片到相册',
|
||
confirmText: '去授权',
|
||
cancelText: '取消',
|
||
success: (res) => {
|
||
if (res.confirm) {
|
||
uni.openSetting()
|
||
}
|
||
},
|
||
})
|
||
},
|
||
|
||
/**
|
||
* 实际保存图片的方法
|
||
*/
|
||
saveImageToAlbum() {
|
||
uni.showLoading({
|
||
title: '保存中...',
|
||
mask: true,
|
||
})
|
||
uni.canvasToTempFilePath({
|
||
canvasId: 'posterCanvas',
|
||
success: (res) => {
|
||
uni.hideLoading()
|
||
uni.saveImageToPhotosAlbum({
|
||
filePath: res.tempFilePath,
|
||
success: () => {
|
||
uni.showToast({
|
||
title: '已保存至相册,去朋友圈分享',
|
||
icon: 'none',
|
||
})
|
||
},
|
||
fail: (err) => {
|
||
console.log('保存失败:', err)
|
||
// 检查是否是权限问题
|
||
if (err.errMsg.indexOf('auth deny') > -1) {
|
||
// #ifdef MP-WEIXIN
|
||
this.showAuthModal()
|
||
// #endif
|
||
} else {
|
||
uni.showToast({
|
||
title: '保存失败',
|
||
icon: 'none',
|
||
})
|
||
}
|
||
},
|
||
})
|
||
},
|
||
fail: (err) => {
|
||
console.log('生成临时文件失败:', err)
|
||
uni.showToast({
|
||
title: '生成图片失败',
|
||
icon: 'none',
|
||
})
|
||
},
|
||
},
|
||
this
|
||
)
|
||
},
|
||
},
|
||
}
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
@font-face {
|
||
font-family: 'YouSheBiaoTiHei';
|
||
src: url('https://hnxix-public.oss-cn-hangzhou.aliyuncs.com/driver/wx/font/YouSheBiaoTiHei.ttf');
|
||
}
|
||
|
||
.test {
|
||
font-family: 'YouSheBiaoTiHei';
|
||
}
|
||
|
||
/* 海报容器 */
|
||
.share-poster {
|
||
// background-color: #fff;
|
||
border-radius: 20rpx;
|
||
max-height: 60vh;
|
||
// overflow-y: auto;
|
||
|
||
/* 画布样式 */
|
||
.poster-canvas {
|
||
display: block;
|
||
margin: auto;
|
||
// background: #fff;
|
||
}
|
||
|
||
}
|
||
|
||
/* 分享按钮组 */
|
||
.share-actions {
|
||
position: absolute;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: -200rpx;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 20rpx 30rpx;
|
||
|
||
.action-item {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
padding: 20rpx 30rpx;
|
||
border-radius: 40rpx;
|
||
min-width: 80rpx;
|
||
font-size: 28rpx;
|
||
}
|
||
|
||
.left {
|
||
background-color: #0ed05f;
|
||
color: #ffffff;
|
||
}
|
||
|
||
.middle {
|
||
background-color: #1aad19;
|
||
color: #ffffff;
|
||
}
|
||
|
||
.right {
|
||
background-color: #ffffff;
|
||
color: #000000;
|
||
}
|
||
}
|
||
|
||
// ::v-deep .u-popup {
|
||
// background-color: rebeccapurple !important;
|
||
// }
|
||
|
||
// ::v-deep .u-popup__content {
|
||
// background-color: rebeccapurple !important;
|
||
// }
|
||
::v-deep .wd-popup-wrapper .wd-popup {
|
||
overflow-y: visible !important;
|
||
}
|
||
|
||
::v-deep(.wd-popup-wrapper .wd-popup) {
|
||
overflow-y: visible !important;
|
||
}
|
||
|
||
:deep(.my-custom-popup.wd-popup) {
|
||
/* 样式 */
|
||
overflow-y: visible !important;
|
||
}
|
||
</style> |