535 lines
13 KiB
Vue
535 lines
13 KiB
Vue
<template>
|
||
<view>
|
||
<u-popup :safeAreaInsetBottom='false' :show="show" @close="onClose" mode="center" :round="10">
|
||
<view class="share-poster">
|
||
<!-- 海报画布 -->
|
||
<canvas class="poster-canvas" canvas-id="posterCanvas"
|
||
:style="{width: posterWidth + 'px',height: posterHeight + 'px',}"></canvas>
|
||
|
||
<!-- 分享操作按钮 -->
|
||
<view class="share-actions">
|
||
<view class="action-item left" @click="shareToFriend">
|
||
微信分享
|
||
</view>
|
||
|
||
<view class="action-item right" @click="savePoster">
|
||
保存相册
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</u-popup>
|
||
</view>
|
||
</template>
|
||
|
||
<script>
|
||
import {
|
||
RESOURCES_URL
|
||
} from '@/config/app'
|
||
/**
|
||
* @property {Object} goodsInfo - 商品信息
|
||
* @property {string} goodsInfo.title - 商品标题
|
||
* @property {number} goodsInfo.price - 商品价格
|
||
* @property {string} goodsInfo.image - 商品主图URL
|
||
* @property {string} goodsInfo.qrcode - 小程序码URL
|
||
* @property {boolean} show - 控制弹窗显示隐藏
|
||
* @event updateShare - 弹窗关闭时触发,参数为false
|
||
*/
|
||
export default {
|
||
name: 'SharePoster',
|
||
props: {
|
||
goodsInfo: {
|
||
type: Object,
|
||
required: true,
|
||
|
||
default: () => ({
|
||
title: '',
|
||
price: 0,
|
||
image: '',
|
||
qrcode: '', // 小程序码URL
|
||
}),
|
||
},
|
||
show: {
|
||
type: Boolean,
|
||
default: false,
|
||
},
|
||
},
|
||
data() {
|
||
return {
|
||
generating: false, // 海报生成状态
|
||
posterWidth: 300, // 减小画布宽度
|
||
posterHeight: 450, // 减小画布高度
|
||
loadingText: {
|
||
contentdown: '生成海报中...',
|
||
contentrefresh: '生成海报中...',
|
||
contentnomore: '生成完成',
|
||
},
|
||
imgUrl: '',
|
||
}
|
||
},
|
||
watch: {
|
||
// 监听show变化,显示时生成海报
|
||
show(newVal) {
|
||
if (newVal) {
|
||
this.$nextTick(() => {
|
||
this.generatePoster()
|
||
})
|
||
}
|
||
},
|
||
goodsInfo: {
|
||
immediate: true,
|
||
deep: true,
|
||
handler(newVal) {
|
||
console.log('SharePoster goodsInfo 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.goodsInfo.image || ''
|
||
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 share_title =
|
||
'https://hnxix-public.oss-cn-hangzhou.aliyuncs.com/driver/wx/img/share_title.png'
|
||
if (share_title) {
|
||
const qrSize = 50
|
||
const qrX = 15
|
||
const qrY = 20 // 放在中间位置
|
||
await this.drawImage(ctx, share_title, qrX, qrY, 100, 24)
|
||
}
|
||
|
||
|
||
// 绘制标题和品牌
|
||
const gradient = ctx.createLinearGradient(15, 110, 15, 150)
|
||
gradient.addColorStop(0, '#1068FF'); // 底部开始
|
||
gradient.addColorStop(0.48, '#2B497E'); // 48%位置
|
||
gradient.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(19)
|
||
ctx.setTextAlign('left')
|
||
ctx.fillStyle = gradient
|
||
ctx.fillText('王师傅', 15, 120)
|
||
|
||
// 绘制副标题
|
||
ctx.font = 'italic bold 20px sans-serif'
|
||
ctx.setFontSize(19)
|
||
ctx.setTextAlign('left')
|
||
ctx.fillStyle = gradient
|
||
ctx.fillText('邀请您预约行程', 15, 150)
|
||
|
||
// 重置阴影效果
|
||
ctx.shadowColor = 'transparent'
|
||
ctx.shadowBlur = 0
|
||
ctx.shadowOffsetX = 0
|
||
ctx.shadowOffsetY = 0
|
||
|
||
// 绘制小程序码 - 放在中下部位置
|
||
const qrcodeUrl = this.goodsInfo.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) => {
|
||
uni.showShareImageMenu({
|
||
path: res.tempFilePath,
|
||
success: function() {
|
||
console.log('分享图片成功')
|
||
},
|
||
fail: function(err) {
|
||
console.log('分享图片失败:', err)
|
||
},
|
||
})
|
||
},
|
||
},
|
||
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.canvasToTempFilePath({
|
||
canvasId: 'posterCanvas',
|
||
success: (res) => {
|
||
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');
|
||
}
|
||
|
||
/* 海报容器 */
|
||
.share-poster {
|
||
outline: none;
|
||
// background-color: #fff;
|
||
border-radius: 20rpx;
|
||
overflow: hidden;
|
||
max-height: 60vh;
|
||
// overflow-y: auto;
|
||
// padding-bottom: 140rpx; // 为底部按钮留出空间
|
||
|
||
/* 画布样式 */
|
||
.poster-canvas {
|
||
display: block;
|
||
margin: auto;
|
||
// background: #fff;
|
||
}
|
||
|
||
/* 分享按钮组 */
|
||
.share-actions {
|
||
position: fixed;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 140rpx;
|
||
display: flex;
|
||
justify-content: space-around;
|
||
align-items: center;
|
||
padding: 20rpx 40rpx;
|
||
|
||
.action-item {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
padding: 28rpx 40rpx;
|
||
border-radius: 40rpx;
|
||
|
||
}
|
||
|
||
.left {
|
||
background-color: #0ED05F;
|
||
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;
|
||
// }
|
||
</style> |