staff/components/SharePoster.vue

631 lines
16 KiB
Vue
Raw Permalink 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 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>