1.需求介绍
- 本系统的邀请地址放在二维码中,被邀请人扫描后可注册为其团队。
- 背景图中会有一处正方形的空白,这个空白就是需要二维码来填充,然后合成邀请海报,并且转为图片提供给用户下载
- 现在有多个背景图,需要作海报背景图的切换。
2.实现
2.1 二维码的生成
<script setup>
const painterRef = ref(null); // 海报画板组件的实例对象
const showPainter = ref(true)
const painterImageUrl = ref(); // 海报 url
// 渲染函数
const renderPoster = async (poster) => {
await painterRef.value.render(poster);
}
// 制作二维码
const markCode = (text) => {
renderPoster({
css: {
width: `${state.qrcodeSize}px`,
height: `${state.qrcodeSize}px`
},
views:[
{
type: 'qrcode',
text: text,
css: {
width: `${state.qrcodeSize}px`,
height: `${state.qrcodeSize}px`
}
}
]
})
}
// 获得生成的图片
const setPainterImageUrl = (path) => {
// 此时在这里,二维码已经被转换为图片了,这个图片的地址是浏览器临时地址。
painterImageUrl.value = path;
showPainter.value = false
// 省略canvas渲染函数,下一部分贴全部代码。
};
// 复制邀请链接
// #ifdef H5
const onInviteRegister = () => {
const rootUrl = getRootUrl(); // 获取H5跟地址
const realUrl = rootUrl.includes('localhost') ? 'http://192.168.2.19:3000/' : rootUrl;
// userInfo?.value.inviteCode 存储在pinia中的个人邀请码。
return realUrl + 'pages/auth/register?inviteCode=' + userInfo?.value.inviteCode
}
// #endif
onMounted(async () => {
const url = onInviteRegister()
markCode(url)
})
/**
* 获取H5-真实根地址 兼容hash+history模式
*/
export function getRootUrl() {
let url = '';
// #ifdef H5
url = location.origin + '/';
if (location.hash !== '') {
url += '#/';
}
// #endif
return url;
}
</script>
<template>
<!-- 海报画板:默认隐藏只用来生成海报。生成方式为主动调用 -->
<l-painter
v-if="showPainter"
isCanvasToTempFilePath
pathType="url"
@success="setPainterImageUrl"
hidden
ref="painterRef"
/>
</template>
2.2 海报生成。
- 利用的是
uni.createCanvasContextApi 和 canvas元素
- 二维码合并在背景图中,然后在h5中下载这个邀请海报
<script setup>
// 海报尺寸比例
const posterRatio = 2000 / 1524
// 背景图大小
const posterSize = {
width: 1524,
height: 2000
}
const state = reactive({
qrcodeSize: 630,
// 二维码在海报中的位置比例
qrcodePosition: {
x: 447 / posterSize.width, // 水平位置比例
y: 1221 / posterSize.height // 垂直位置比例
}
})
const painterImageUrl = ref(); // 海报 url
const pixelRatio = ref(1)
const scale = ref(1) // 缩放比例
// 生成海报背景图
const drawPoster = async () => {
if(!painterImageUrl.value) {
return uni.showToast({
title: '二维码生成中,请稍后...',
icon: 'none'
})
}
await uni.showLoading({
title: '海报生成中...',
mask: true
})
try {
// 创建canvas上下文
const ctx = uni.createCanvasContext("mycanvas")
// 清空画布
ctx.clearRect(0, 0, canvasWidth.value, canvasHeight.value)
// 绘制背景图
ctx.drawImage(currentPoster.value, 0, 0, canvasWidth.value, canvasHeight.value)
// 计算二维码位置和大小
const qrcodeSize = state.qrcodeSize * scale.value
const qrcodeX = canvasWidth.value * state.qrcodePosition.x
const qrcodeY = canvasHeight.value * state.qrcodePosition.y
// 绘制二维码
ctx.drawImage(painterImageUrl.value, qrcodeX, qrcodeY, qrcodeSize, qrcodeSize)
// 执行绘制
ctx.draw(false, () => {
setTimeout(() => {
// 将canvas转为图片
uni.canvasToTempFilePath({
canvasId: 'mycanvas',
success: (res) => {
isPosterShow.value = true
// 保存临时路径用于分享或保存
state.tempFilePath = res.tempFilePath
uni.hideLoading()
},
fail: (err) => {
console.error('Canvas转图片失败:', err)
uni.hideLoading()
}
})
}, 300)
})
} catch(e) {
console.error('绘制海报失败:', error)
uni.hideLoading()
}
}
// 计算画布尺寸
const calculateCanvasSize = () => {
const systemInfo = uni.getSystemInfoSync()
const screenWidth = systemInfo.windowWidth
// const screenHeight = systemInfo.windowHeight
// 修改计算逻辑,保持原始比例
// 使用固定比例,确保海报完整显示且不变形
canvasWidth.value = screenWidth
// canvasHeight.value = screenWidth * posterRatio
canvasHeight.value = (screenWidth / 3) * 4 * posterRatio
// 计算缩放比例
scale.value = canvasWidth.value / posterSize.width
// 设置像素比
pixelRatio.value = systemInfo.pixelRatio || 1
}
// 保存图片方法
const saveImage = () => {
// #ifdef H5
uni.showLoading({
title: '正在下载中...',
mask: true
})
const xhr = new XMLHttpRequest();
xhr.open('GET', state.tempFilePath, true);
xhr.responseType = 'arraybuffer'; // 返回类型blob
xhr.onload = function () {
if (xhr.readyState === 4 && xhr.status === 200) {
let blob = this.response;
// 转换一个blob链接
let u = window.URL.createObjectURL(new Blob([blob],{ type: 'image/png' })) // 视频的type是video/mp4,图片是image/jpeg
let a = document.createElement('a');
a.download = 'image'; // 设置下载的文件名
a.href = u;
a.style.display = 'none'
document.body.appendChild(a)
a.click();
uni.hideLoading()
document.body.removeChild(a);
}
};
xhr.send()
// #endif
};
onLoad(() => {
calculateCanvasSize()
})
</script>
<template>
<view>
<view class="poster-wrapper">
<canvas
:disable-scroll="false"
canvas-id="mycanvas"
id="mycanvas"
:style="{
width: canvasWidth + 'px',
height: canvasHeight + 'px'
}"
></canvas>
</view>
<view class="button-wrapper">
<button class="save-button" @click="saveImage">保存图片</button>
</view>
</view>
</template>
2.3 缩略图选择,更换海报背景图
- 缩略图展示多个背景图,用户可以随意切换海报的背景图。
- 缩略图默认隐藏,当点击顶部的文字时,则显示缩略图。
- 点击缩略图以外的位置,则隐藏缩略图。
<script setup>
// 如果缩略图太多的情况下,可以使用动态引入的方法,就不需要一个一个的引入了。
import poster1 from '@/static/img/poster1.jpg'
import poster2 from '@/static/img/poster2.jpg'
import poster3 from '@/static/img/poster3.jpg'
// 海报背景图列表
const posterList = [
// 如下每项的src属性这是背景图的地址,name属性则是缩略图的名称
{ id: 1, src: poster1, name: '背景1' },
{ id: 2, src: poster2, name: '背景2' },
{ id: 3, src: poster3, name: '背景3' },
]
// 当前选中的背景图
const currentPoster = ref(posterList[0].src)
// 是否显示缩略图选择器
const showThumbnails = ref(false)
// 切换海报背景
const changePosterBackground = (poster) => {
if(currentPoster.value === poster) return;
currentPoster.value = poster
drawPoster()
}
// 切换缩略图显示状态
const toggleThumbnails = () => {
showThumbnails.value = !showThumbnails.value
}
// 点击海报区域
const handlePosterClick = (e) => {
// 获取点击位置相对于屏幕顶部的距离
const clickY = e.clientY || e.touches[0].clientY
// 如果点击位置在缩略图区域之外,则隐藏缩略图
if (clickY > 180 && showThumbnails.value) {
showThumbnails.value = false
} else if (clickY <= 180) {
// 点击缩略图区域,显示缩略图
showThumbnails.value = true
}
}
</script>
<template>
<view class="container" @click="handlePosterClick">
<!-- 缩略图选择器 -->
<view class="thumbnails-wrapper" :class="{ 'show': showThumbnails }">
<view class="thumbnails-container">
<view
v-for="poster in posterList"
:key="poster.id"
:class="['thumbnail-item', { 'active': currentPoster.id === poster.id }]"
@click.stop="changePosterBackground(poster.src)"
>
<image :src="poster.src" mode="aspectFill" class="thumbnail-image" />
<text class="thumbnail-name">{{ poster.name }}</text>
</view>
</view>
</view>
<!-- 提示信息 -->
<view class="thumbnail-hint" @click.stop="toggleThumbnails">
<text>{{ showThumbnails ? '点击选择背景' : '点击此处选择背景' }}</text>
</view>
</view>
</template>
3.总结
- 背景图的比例是
750 * 1300,
- 背景图的二维码留空的左上角的起始位置:
x: 447, y: 1221
- 完整代码如下。
<script setup>
import { reactive, ref, computed, onMounted } from 'vue';
import { onLoad } from '@dcloudio/uni-app';
import sheep from '@/sheep';
import poster1 from '@/static/img/poster1.jpg'
import poster2 from '@/static/img/poster2.jpg'
import poster3 from '@/static/img/poster3.jpg'
// 用户信息
const userInfo = computed(() => sheep.$store('user').userInfo);
const isPosterShow = ref(false)
const canvasWidth = ref(0)
const canvasHeight = ref(0)
// 海报背景图列表
const posterList = [
{ id: 1, src: poster1, name: '背景1' },
{ id: 2, src: poster2, name: '背景2' },
{ id: 3, src: poster3, name: '背景3' },
]
// 当前选中的背景图
const currentPoster = ref(posterList[0].src)
// 是否显示缩略图选择器
const showThumbnails = ref(false)
// 海报尺寸比例
const posterRatio = 2000 / 1524
const posterSize = {
width: 1524,
height: 2000
}
const state = reactive({
qrcodeSize: 630,
// 二维码在海报中的位置比例
qrcodePosition: {
x: 447 / posterSize.width, // 水平位置比例
y: 1221 / posterSize.height // 垂直位置比例
}
})
const painterRef = ref(); // 海报画板
const showPainter = ref(true)
const painterImageUrl = ref(); // 海报 url
const pixelRatio = ref(1)
const scale = ref(1) // 缩放比例
// 生成海报背景图
const drawPoster = async () => {
if(!painterImageUrl.value) {
return uni.showToast({
title: '二维码生成中,请稍后...',
icon: 'none'
})
}
await uni.showLoading({
title: '海报生成中...',
mask: true
})
try {
// 创建canvas上下文
const ctx = uni.createCanvasContext("mycanvas")
// 清空画布
ctx.clearRect(0, 0, canvasWidth.value, canvasHeight.value)
// 绘制背景图
ctx.drawImage(currentPoster.value, 0, 0, canvasWidth.value, canvasHeight.value)
// 计算二维码位置和大小
const qrcodeSize = state.qrcodeSize * scale.value
const qrcodeX = canvasWidth.value * state.qrcodePosition.x
const qrcodeY = canvasHeight.value * state.qrcodePosition.y
// 绘制二维码
ctx.drawImage(painterImageUrl.value, qrcodeX, qrcodeY, qrcodeSize, qrcodeSize)
// 执行绘制
ctx.draw(false, () => {
setTimeout(() => {
// 将canvas转为图片
uni.canvasToTempFilePath({
canvasId: 'mycanvas',
success: (res) => {
isPosterShow.value = true
// 保存临时路径用于分享或保存
state.tempFilePath = res.tempFilePath
uni.hideLoading()
},
fail: (err) => {
console.error('Canvas转图片失败:', err)
uni.hideLoading()
}
})
}, 300)
})
} catch(e) {
console.error('绘制海报失败:', error)
uni.hideLoading()
}
}
// 渲染海报
const renderPoster = async (poster) => {
await painterRef.value.render(poster);
};
// 获得生成的图片
const setPainterImageUrl = (path) => {
painterImageUrl.value = path;
showPainter.value = false
// 二维码生成后立即绘制海报
drawPoster()
};
/**
* 生成核销二维码
*/
const markCode = (text) => {
renderPoster({
css: {
width: `${state.qrcodeSize}px`,
height: `${state.qrcodeSize}px`
},
views:[
{
type: 'qrcode',
text: text,
css: {
width: `${state.qrcodeSize}px`,
height: `${state.qrcodeSize}px`
}
}
]
})
}
// 复制邀请链接
// #ifdef H5
const onInviteRegister = () => {
const rootUrl = getRootUrl();
const realUrl = rootUrl.includes('localhost') ? 'http://192.168.2.19:3000/' : rootUrl;
return realUrl + 'pages/auth/register?inviteCode=' + userInfo?.value.inviteCode
}
// #endif
// 保存图片方法
const saveImage = () => {
// #ifdef H5
uni.showLoading({
title: '正在下载中...',
mask: true
})
const xhr = new XMLHttpRequest();
xhr.open('GET', state.tempFilePath, true);
xhr.responseType = 'arraybuffer'; // 返回类型blob
xhr.onload = function () {
if (xhr.readyState === 4 && xhr.status === 200) {
let blob = this.response;
// 转换一个blob链接
let u = window.URL.createObjectURL(new Blob([blob],{ type: 'image/png' })) // 视频的type是video/mp4,图片是image/jpeg
let a = document.createElement('a');
a.download = 'image'; // 设置下载的文件名
a.href = u;
a.style.display = 'none'
document.body.appendChild(a)
a.click();
uni.hideLoading()
document.body.removeChild(a);
}
};
xhr.send()
// #endif
};
// 计算画布尺寸
const calculateCanvasSize = () => {
const systemInfo = uni.getSystemInfoSync()
const screenWidth = systemInfo.windowWidth
// const screenHeight = systemInfo.windowHeight
// 修改计算逻辑,保持原始比例
// 使用固定比例,确保海报完整显示且不变形
canvasWidth.value = screenWidth
// canvasHeight.value = screenWidth * posterRatio
canvasHeight.value = (screenWidth / 3) * 4 * posterRatio
// 计算缩放比例
scale.value = canvasWidth.value / posterSize.width
// 设置像素比
pixelRatio.value = systemInfo.pixelRatio || 1
}
// 切换海报背景
const changePosterBackground = (poster) => {
if(currentPoster.value === poster) return;
currentPoster.value = poster
drawPoster()
}
// 切换缩略图显示状态
const toggleThumbnails = () => {
showThumbnails.value = !showThumbnails.value
}
// 点击海报区域
const handlePosterClick = (e) => {
// 获取点击位置相对于屏幕顶部的距离
const clickY = e.clientY || e.touches[0].clientY
// 如果点击位置在缩略图区域之外,则隐藏缩略图
if (clickY > 180 && showThumbnails.value) {
showThumbnails.value = false
} else if (clickY <= 180) {
// 点击缩略图区域,显示缩略图
showThumbnails.value = true
}
}
onMounted(async () => {
const url = onInviteRegister()
markCode(url)
})
onLoad(() => {
calculateCanvasSize()
})
/**
* 获取H5-真实根地址 兼容hash+history模式
*/
export function getRootUrl() {
let url = '';
// #ifdef H5
url = location.origin + '/';
if (location.hash !== '') {
url += '#/';
}
// #endif
return url;
}
</script>
<template>
<view class="container" @click="handlePosterClick">
<!-- 缩略图选择器 -->
<view class="thumbnails-wrapper" :class="{ 'show': showThumbnails }">
<view class="thumbnails-container">
<view
v-for="poster in posterList"
:key="poster.id"
:class="['thumbnail-item', { 'active': currentPoster.id === poster.id }]"
@click.stop="changePosterBackground(poster.src)"
>
<image :src="poster.src" mode="aspectFill" class="thumbnail-image" />
<text class="thumbnail-name">{{ poster.name }}</text>
</view>
</view>
</view>
<!-- 提示信息 -->
<view class="thumbnail-hint" @click.stop="toggleThumbnails">
<text>{{ showThumbnails ? '点击选择背景' : '点击此处选择背景' }}</text>
</view>
<view class="poster-wrapper">
<canvas
:disable-scroll="false"
canvas-id="mycanvas"
id="mycanvas"
:style="{
width: canvasWidth + 'px',
height: canvasHeight + 'px'
}"
></canvas>
</view>
<view class="button-wrapper">
<button class="save-button" @click="saveImage">保存图片</button>
</view>
<!-- 海报画板:默认隐藏只用来生成海报。生成方式为主动调用 -->
<l-painter
v-if="showPainter"
isCanvasToTempFilePath
pathType="url"
@success="setPainterImageUrl"
hidden
ref="painterRef"
/>
</view>
</template>
<style scoped lang="scss">
.container {
min-height: 100vh;
width: 100vw;
background-color: #000;
position: relative;
display: flex;
flex-direction: column;
overflow: auto;
}
.thumbnails-wrapper {
position: fixed;
top: 0;
left: 0;
right: 0;
background-color: rgba(0, 0, 0, 0.8);
z-index: 100;
padding: 20rpx;
transform: translateY(-100%);
transition: transform 0.3s ease;
&.show {
transform: translateY(0);
}
}
.thumbnails-container {
display: flex;
justify-content: space-around;
align-items: center;
padding: 10rpx 0;
}
.thumbnail-item {
display: flex;
flex-direction: column;
align-items: center;
width: 150rpx;
opacity: 0.7;
transition: all 0.2s ease;
&.active {
opacity: 1;
transform: scale(1.05);
}
}
.thumbnail-image {
width: 120rpx;
height: 160rpx;
border-radius: 8rpx;
border: 2rpx solid transparent;
.active & {
border-color: rgb(195, 32, 33);
}
}
.thumbnail-name {
font-size: 24rpx;
color: #fff;
margin-top: 10rpx;
}
.thumbnail-hint {
height: 60rpx;
display: flex;
justify-content: center;
align-items: center;
color: rgba(255, 255, 255, 0.7);
font-size: 24rpx;
background-color: rgba(0, 0, 0, 0.5);
border-radius: 0 0 16rpx 16rpx;
}
.poster-wrapper {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: auto;
overflow: visible;
margin-top: 20rpx;
}
.button-wrapper {
position: fixed;
bottom: 32rpx;
left: 150rpx;
right: 150rpx;
z-index: 10;
}
.save-button {
background-color: rgb(195, 32, 33);
color: #fff;
border-radius: 40rpx;
height: 88rpx;
line-height: 88rpx;
font-size: 32rpx;
font-weight: bold;
border: none;
&:active {
opacity: 0.8;
}
}
</style>
最后
- 如有不正之处,请佬不吝赐教,万分感谢。
- 祝大家向上!