279 lines
8.3 KiB
Vue
279 lines
8.3 KiB
Vue
<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> |