Files
cashier_wx/distribution/poster.vue

279 lines
8.3 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="poster-page">
<view class="preview-container" v-if="posterUrl">
<image :src="posterUrl" mode="widthFix" class="poster-img"></image>
<button class="save-btn" @click="saveToAlbum">保存到相册</button>
</view>
<button class="generate-btn" @click="generatePoster" v-if="!posterUrl">生成海报</button>
<canvas
id="posterCanvas"
type="2d"
:style="{ width: canvasWidth + 'px', height: canvasHeight + 'px' }"
class="canvas-hidden"
></canvas>
</view>
</template>
<script setup>
import { ref, getCurrentInstance } from 'vue'
import { onReady } from '@dcloudio/uni-app'
// 基础变量(不变)
const canvasWidth = ref(375)
const canvasHeight = ref(667)
const posterUrl = ref('')
let canvasNode = null
let ctx = null
const instance = getCurrentInstance()
// 海报配置(不变)
const posterConfig = {
bgImage: 'https://cashier-oss.oss-cn-beijing.aliyuncs.com/upload/1/677c4a5ae43a45eb98c0ae1a6d242021.png',
title: { text: '限时优惠活动', x: 20, y: 40, fontSize: 24, color: '#fff', fontWeight: 'bold' },
desc: { text: '全场商品满200减50限时3天', x: 20, y: 80, fontSize: 16, color: '#fff' },
mainImage: { url: 'https://img.yzcdn.cn/vant/apple-1.jpg', x: 20, y: 120, width: 335, height: 335, radius: 10 },
qrcode: { url: 'https://czg-oss.oss-cn-hangzhou.aliyuncs.com/catering/store/HQ200龙虾仔.JPG', x: 265, y: 500, width: 90, height: 90 },
footerText: { text: '扫码立即参与活动', x: 20, y: 550, fontSize: 14, color: '#333' }
}
// 初始化 Canvas不变
onReady(() => {
const query = uni.createSelectorQuery().in(instance)
query
.select('#posterCanvas')
.fields({ node: true, size: true })
.exec((res) => {
if (!res[0] || !res[0].node) {
console.error('未找到 Canvas 节点')
uni.showToast({ title: 'Canvas节点初始化失败', icon: 'none' })
return
}
canvasNode = res[0].node
canvasNode.width = canvasWidth.value
canvasNode.height = canvasHeight.value
ctx = canvasNode.getContext('2d')
if (!ctx) {
console.error('获取 2D 上下文失败')
uni.showToast({ title: 'Canvas上下文初始化失败', icon: 'none' })
}
})
})
// 绘制背景(依赖修复后的 drawImage
const drawBackground = () => {
return new Promise((resolve, reject) => {
if (!posterConfig.bgImage) return reject('无背景图')
drawImage({
url: posterConfig.bgImage,
x: 0,
y: 0,
width: canvasWidth.value,
height: canvasHeight.value
}).then(resolve).catch(reject)
})
}
// 绘制文字(不变)
const drawText = (options) => {
const { text, x, y, fontSize, color, fontWeight = 'normal', fontFamily = 'sans-serif' } = options
ctx.font = `${fontWeight} ${fontSize}px ${fontFamily}`
ctx.fillStyle = color
ctx.fillText(text, x, y)
}
// 绘制圆角矩形(不变)
const drawRoundRect = (x, y, width, height, radius) => {
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()
}
// ---------------------- 核心修复drawImage 方法(无需 Image 实例) ----------------------
const drawImage = (options) => {
return new Promise((resolve, reject) => {
const { url, x, y, width, height, radius = 0 } = options
if (!url) return reject('图片路径为空')
if (width <= 0 || height <= 0) return reject(`图片尺寸无效:${width}x${height}`)
// 关键:确保 canvasNode 已初始化(否则无法创建图片对象)
if (!canvasNode) return reject('Canvas 节点未初始化,无法创建图片对象')
// 1. 获取图片临时路径(不变)
uni.getImageInfo({
src: url,
success: (imgInfo) => {
if (imgInfo.width <= 0 || imgInfo.height <= 0) {
return reject(`无效图片:${url}`)
}
// 2. 关键修复:用 Canvas 节点的 createImage() 方法创建图片对象
// 这是微信小程序 Canvas 2D 节点原生支持的方法,类型完全匹配
const image = canvasNode.createImage()
// 3. 监听图片加载完成(接口与标准 Image 一致)
image.onload = () => {
try {
if (radius > 0) {
ctx.save()
drawRoundRect(x, y, width, height, radius)
ctx.clip()
}
// 4. 绘制图片:传入 Canvas 节点创建的图片实例(类型匹配)
ctx.drawImage(image, x, y, width, height)
if (radius > 0) ctx.restore()
resolve()
} catch (drawErr) {
reject(`绘制图片失败:${drawErr.message}`)
}
}
// 监听加载失败
image.onerror = (err) => {
reject(`图片加载失败:${err.message || '未知错误'}`)
}
// 5. 赋值临时路径,触发加载
image.src = imgInfo.path
},
fail: (err) => {
reject(`获取图片信息失败:${err.errMsg}(路径:${url}`)
}
})
})
}
// 生成海报(不变)
const generatePoster = async () => {
try {
if (!canvasNode || !ctx) throw new Error('Canvas 未初始化完成')
ctx.clearRect(0, 0, canvasWidth.value, canvasHeight.value)
// 绘制背景(失败则用纯色兜底)
await drawBackground().catch(() => {
ctx.fillStyle = '#f5f5f5'
ctx.fillRect(0, 0, canvasWidth.value, canvasHeight.value)
})
// 绘制文字
drawText(posterConfig.title)
drawText(posterConfig.desc)
drawText(posterConfig.footerText)
// 绘制图片(主图 + 二维码)
await drawImage(posterConfig.mainImage)
await drawImage(posterConfig.qrcode)
// 延迟确保绘制生效
await new Promise(resolve => setTimeout(resolve, 200))
await convertToImage()
uni.showToast({ title: '海报生成成功', icon: 'success' })
} catch (err) {
console.error('生成失败:', err)
uni.showToast({ title: `生成失败:${err.message}`, icon: 'none' })
}
}
// 转图片(不变)
const convertToImage = () => {
return new Promise((resolve, reject) => {
if (!canvasNode) return reject('Canvas 节点不存在')
uni.canvasToTempFilePath({
canvas: canvasNode,
width: canvasWidth.value,
height: canvasHeight.value,
destWidth: canvasWidth.value * 2,
destHeight: canvasHeight.value * 2,
success: (res) => {
posterUrl.value = res.tempFilePath
resolve()
},
fail: (err) => {
reject(`转图片失败:${err.errMsg}`)
}
}, instance)
})
}
// 保存相册相关方法(不变)
const saveToAlbum = () => {
if (!posterUrl.value) return
uni.getSetting({
success: (res) => {
if (!res.authSetting['scope.writePhotosAlbum']) {
uni.authorize({
scope: 'scope.writePhotosAlbum',
success: saveImage,
fail: () => {
uni.showModal({
title: '权限申请',
content: '需要相册权限才能保存海报',
success: (modalRes) => modalRes.confirm && uni.openSetting()
})
}
})
} else {
saveImage()
}
}
})
}
const saveImage = () => {
uni.saveImageToPhotosAlbum({
filePath: posterUrl.value,
success: () => uni.showToast({ title: '保存成功', icon: 'success' }),
fail: (err) => {
console.error('保存失败:', err)
uni.showToast({ title: '保存失败', icon: 'none' })
}
})
}
</script>
<style scoped>
/* 样式不变 */
.poster-page {
display: flex;
flex-direction: column;
align-items: center;
padding: 20rpx;
}
.canvas-hidden {
position: absolute;
left: -9999rpx;
top: -9999rpx;
z-index: -1;
width: 375px;
height: 667px;
}
.preview-container {
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
}
.poster-img {
width: 100%;
border-radius: 16rpx;
box-shadow: 0 4rpx 10rpx rgba(0, 0, 0, 0.1);
}
.generate-btn, .save-btn {
width: 600rpx;
height: 88rpx;
line-height: 88rpx;
margin-top: 40rpx;
background-color: #07c160;
color: #ffffff;
font-size: 32rpx;
border-radius: 44rpx;
}
.save-btn {
background-color: #1677ff;
margin-top: 20rpx;
}
</style>