多端生成分享海报
微信小程序 / H5 / PC 三端生成海报
1. 微信小程序生成分享海报(朋友圈 QQ)
点我查看代码
<!--
* Love and Peace
* Description:
* Components: 微信分享海报组件
* URL:
* NOTE:
* 组件在最下方隐藏生成(因为生成过程和最后的分享调起分享弹窗会双层弹窗,看着很跳)
* 这里用的是之前的比例 可以直接使用,如果尺寸不一致可以再加参数进行微调
example:
<WxSavePoster v-if="props.isWeapp && visible" poster-image-url="https://static.nowcoder.com/fe/file/oss/1716176092033TRSDY.png" :qrcode-image-config="qrcodeImageConfig" @close="close"></WxSavePoster>
-->
<template>
<div class="save-image-popup">
<div class="poster-main" :style="{width: `${canvasBoxStyle.width}px`, height: `${canvasBoxStyle.height}px`}">
<canvas id="saveImageCanvas" canvas-id="saveImageCanvas" type="2d" width="100%" height="100%"></canvas>
</div>
</div>
</template>
<script lang="ts" setup>
import Taro from '@tarojs/taro';
import {onMounted, reactive} from 'vue';
const emits = defineEmits(['close']);
const props = defineProps({
posterImageUrl: {type: String, required: true},
// qrcodeImageConfig example:
// const qrcodeImageConfig = ref({
// url: 'https://local.nowcoder.com/static/school-logo.jpeg',
// width: 80,
// height: 80,
// leftSkew: 0, // 左侧偏移 (用于矫正二维码位置)
// topSkew: 0 // 右侧偏移 (用于矫正二维码位置)
// });
qrcodeImageConfig: {type: Object, required: true},
shareDone: {type: Function, required: true}
});
let radio = 0;
let ctx: any = null;
let canvas: any = null;
const canvasBoxStyle: any = reactive({width: 0, height: 0});
const initImage = () => {
Taro.showLoading({title: '生成海报中...'});
wx.createSelectorQuery()
.select('#saveImageCanvas')
.fields({node: true, size: true})
.exec(async res => {
const target = res[0];
canvas = target.node;
ctx = canvas.getContext('2d');
// 调整 Canvas 分辨率: 确保 Canvas 的宽度和高度足够大,以保证图片的清晰度。可以将 Canvas 的尺寸设置为所需输出尺寸的两倍或三倍。
const dpr = wx.getSystemInfoSync().pixelRatio;
canvas.width = target.width * dpr;
canvas.height = target.height * dpr;
ctx.scale(dpr, dpr);
await drawIMG(
// 画背景图
props.posterImageUrl,
0,
0,
canvasBoxStyle.width,
canvasBoxStyle.height
);
// 计算二维码位置
const _qr = props.qrcodeImageConfig;
const qrStyle = {
left: (237 - _qr.leftSkew || 0) * (canvasBoxStyle.width / 344),
top: (549 - _qr.topSkew || 0) * (canvasBoxStyle.height / 656)
};
radio = canvasBoxStyle.width / 344;
await drawIMG(
// 画二维码
_qr.url,
qrStyle.left,
qrStyle.top,
_qr.height * radio,
_qr.width * radio
);
// 保存临时文件,并调起分享
saveImageAndOpenWxShare();
});
};
function drawIMG(src, x, y, width, height) {
return new Promise(resolve => {
let img = canvas.createImage();
img.src = src;
// 这个模拟器好用,但是真机不能➕ 加了报错
// img.width = width;
// img.height = height;
img.onload = () => {
ctx.drawImage(img, x, y, width, height);
resolve(`[${src} ok]`);
};
});
}
const saveImageAndOpenWxShare = () => {
wx.canvasToTempFilePath({
canvas: canvas,
success(res) {
Taro.hideLoading();
console.log(`[canvasToTempFilePath_success_res]`, res);
wx.showShareImageMenu({
path: res.tempFilePath,
success(result) {
props.shareDone({shareType: '小程序分享'});
console.log(`[saveImageAndOpenWxShare_success_result]`, result);
},
fail(result) {
console.log(`[saveImageAndOpenWxShare_fail_result]`, result);
}
});
emits('close');
}
});
};
onMounted(() => {
const SystemInfoSync = Taro.getSystemInfoSync();
const windowHeight = SystemInfoSync.windowHeight;
// const canvasHeight = windowHeight - 188 - 23 - 14;
const canvasHeight = windowHeight - 200;
const canvasWidth = canvasHeight * (344 / 656);
canvasBoxStyle.height = canvasHeight;
canvasBoxStyle.width = canvasWidth;
setTimeout(() => {
initImage();
}, 1000);
});
const close = () => emits('close');
defineExpose({close});
</script>
<style lang="scss">
.save-image-popup {
// 一下属性是为了隐藏升成过程
height: 0px;
width: 0px;
overflow: hidden;
position: absolute;
top: 2000px;
left: 2000px;
.poster-main {
margin: 23px auto 14px auto;
}
.btn-box {
.top-btn {
height: 120px;
margin: 0 12px;
border-radius: 12px;
background: white;
display: flex;
justify-content: space-around;
align-items: center;
.text {
font-size: 12px;
color: #555555;
line-height: 12px;
margin-top: 12px;
}
}
.bottom-btn {
height: 48px;
line-height: 48px;
background: #ffffff;
border-radius: 12px;
margin: 0 12px;
margin-top: 8px;
text-align: center;
color: #a5a5a5;
font-size: 16px;
}
}
}
</style>2. 站内升成分享海报并调用hybird分享图片
点我查看代码
<!--
* Love and Peace
* Description:
* Components: H5分享海报组件
* URL:
* NOTE:
-->
<template>
<div class="h5-save-image-popup">
<div class="poster-main" :style="{width: `${canvasBoxStyle.width}px`, height: `${canvasBoxStyle.height}px`}">
<Canvas id="saveImageCanvas" canvas-id="saveImageCanvas" type="2d" style="height: 100%; width: 100%"></Canvas>
</div>
</div>
</template>
<script lang="ts" setup>
import Taro from '@tarojs/taro';
import {Toast} from '@fe/sdk-hybrid';
import {onMounted, reactive} from 'vue';
import {VCShareImage} from '@/utils/sdk';
import {postUploadImage} from '@/axios/common';
const emits = defineEmits(['close']);
const props = defineProps({
posterImageUrl: {type: String, required: true},
// qrcodeImageConfig example:
// const qrcodeImageConfig = ref({
// url: 'https://local.nowcoder.com/static/school-logo.jpeg',
// width: 80,
// height: 80,
// leftSkew: 0, // 左侧偏移 (用于矫正二维码位置)
// topSkew: 0 // 右侧偏移 (用于矫正二维码位置)
// });
qrcodeImageConfig: {type: Object, required: true},
shareDone: {type: Function, required: true}
});
let radio = 0;
let ctx: any = null;
let canvas: any = null;
const canvasBoxStyle: any = reactive({width: 0, height: 0});
const initImage = async () => {
Taro.showLoading({title: '生成海报中...'});
canvas = document.querySelector('#saveImageCanvas');
ctx = canvas.getContext('2d');
canvas.width = canvasBoxStyle.width;
canvas.height = canvasBoxStyle.height;
// 调整 Canvas 分辨率: 确保 Canvas 的宽度和高度足够大,以保证图片的清晰度。可以将 Canvas 的尺寸设置为所需输出尺寸的两倍或三倍。
const dpr = window.devicePixelRatio;
canvas.width = canvas.width * dpr;
canvas.height = canvas.height * dpr;
ctx.scale(dpr, dpr);
await drawIMG(
// 画背景图
props.posterImageUrl,
0,
0,
canvasBoxStyle.width,
canvasBoxStyle.height
);
// 计算二维码位置
const _qr = props.qrcodeImageConfig;
const qrStyle = {
left: (237 - _qr.leftSkew || 0) * (canvasBoxStyle.width / 344),
top: (549 - _qr.topSkew || 0) * (canvasBoxStyle.height / 656)
};
radio = canvasBoxStyle.width / 344;
await drawIMG(
// 画二维码
_qr.url,
qrStyle.left,
qrStyle.top,
_qr.height * radio,
_qr.width * radio
);
// 升成图片并上传到oss
const blob = await new Promise(resolve => {
canvas.toBlob(resolve, 'image/png');
});
const data = new FormData();
data.append('file', blob as Blob, 'oln-share.png');
const res: any = await postUploadImage({body: data});
if (!res.url) {
Taro.showToast({title: '上传图片失败', icon: 'error'});
return;
}
Taro.hideLoading();
emits('close');
// 调起站内分享
VCShareImage({
src: res.url,
gioExtra: {source_var: '2024老带新'},
call(callData) {
console.log(`[Share.image callData]`, callData);
// 这里看文档之前传的是 boolean 类型,现在改成了 object 类型,所以这里需要判断一下
// 在兼容一下 ios 安卓 数据结构层级问题
const isSuccess = typeof callData === 'boolean' ? callData : callData.isSuccess ?? callData.result?.isSuccess ?? callData.data?.result?.isSuccess ?? false;
if (isSuccess) {
Toast.success('分享成功');
props.shareDone({shareType: `app${callData.media || '分享'}`});
} else {
Toast.error('分享失败');
}
}
});
};
function drawIMG(src, x, y, width, height) {
return new Promise(resolve => {
let img = new Image();
img.src = src;
img.width = width;
img.height = height;
img.crossOrigin = 'anonymous';
img.onload = () => {
ctx.drawImage(img, x, y, width, height);
resolve(`[${src} ok]`);
};
});
}
onMounted(() => {
const SystemInfoSync = Taro.getSystemInfoSync();
const windowHeight = SystemInfoSync.windowHeight;
// const canvasHeight = windowHeight - 188 - 23 - 14;
const canvasHeight = windowHeight - 200;
const canvasWidth = canvasHeight * (344 / 656);
canvasBoxStyle.height = canvasHeight;
canvasBoxStyle.width = canvasWidth;
setTimeout(() => {
initImage();
}, 1000);
});
const close = () => emits('close');
defineExpose({close});
</script>
<style lang="scss">
.h5-save-image-popup {
.poster-main {
margin: 23px auto 14px auto;
}
.btn-box {
.top-btn {
height: 120px;
margin: 0 12px;
border-radius: 12px;
background: white;
display: flex;
justify-content: space-around;
align-items: center;
.text {
font-size: 12px;
color: #555555;
line-height: 12px;
margin-top: 12px;
}
}
.bottom-btn {
height: 48px;
line-height: 48px;
background: #ffffff;
border-radius: 12px;
margin: 0 12px;
margin-top: 8px;
text-align: center;
color: #a5a5a5;
font-size: 16px;
}
}
}
</style>3. PC 生成海报并复制or下载
点我查看代码
<!--
* Love and Peace
* Description:
* Components: pc 分享海报组件
* URL:
* NOTE:
-->
<template>
<div class="pc-save-image-popup">
<div style="height: 23px"></div>
<div class="poster-main-box">
<div class="poster-main" :style="{width: `${canvasBoxStyle.width}px`, height: `${canvasBoxStyle.height}px`}">
<Canvas id="saveImageCanvas" canvas-id="saveImageCanvas" type="2d" style="height: 100%; width: 100%"></Canvas>
</div>
</div>
<div class="btn-box">
<div class="top-btn">
<div class="tw-flex tw-flex-col tw-items-center tw-cursor-pointer" @click="copyPoster">
<div class="icon-box">
<IconZhaopingoutong size="30"></IconZhaopingoutong>
</div>
<div class="text">复制海报图片</div>
</div>
<div class="tw-flex tw-flex-col tw-items-center tw-cursor-pointer" @click="downloadPoster">
<div class="icon-box">
<IconXiazai size="30"></IconXiazai>
</div>
<div class="text">下载海报图片</div>
</div>
</div>
<div class="bottom-btn tw-cursor-pointer" @click="close">取消</div>
</div>
</div>
</template>
<script lang="ts" setup>
import Taro from '@tarojs/taro';
import {onMounted, reactive} from 'vue';
import {IconZhaopingoutong} from '@ncfe/nc.icon.nowpick';
import {IconXiazai} from '@ncfe/nc.icon.sparta';
const emits = defineEmits(['close']);
const props = defineProps({
posterImageUrl: {type: String, required: true},
// qrcodeImageConfig example:
// const qrcodeImageConfig = ref({
// url: 'https://local.nowcoder.com/static/school-logo.jpeg',
// width: 80,
// height: 80,
// leftSkew: 0, // 左侧偏移 (用于矫正二维码位置)
// topSkew: 0 // 右侧偏移 (用于矫正二维码位置)
// });
qrcodeImageConfig: {type: Object, required: true},
shareDone: {type: Function, required: true}
});
let radio = 0;
let ctx: any = null;
let canvas: any = null;
const canvasBoxStyle: any = reactive({width: 0, height: 0});
const initImage = async () => {
canvas = document.querySelector('#saveImageCanvas');
ctx = canvas.getContext('2d');
canvas.width = canvasBoxStyle.width;
canvas.height = canvasBoxStyle.height;
// 调整 Canvas 分辨率: 确保 Canvas 的宽度和高度足够大,以保证图片的清晰度。可以将 Canvas 的尺寸设置为所需输出尺寸的两倍或三倍。
const dpr = window.devicePixelRatio;
console.log(`[dpr]`, dpr);
canvas.width = canvas.width * dpr;
canvas.height = canvas.height * dpr;
ctx.scale(dpr, dpr);
await drawIMG(
// 画背景图
props.posterImageUrl,
0,
0,
canvasBoxStyle.width,
canvasBoxStyle.height
);
// 计算二维码位置
const _qr = props.qrcodeImageConfig;
const qrStyle = {
left: (237 - _qr.leftSkew || 0) * (canvasBoxStyle.width / 344),
top: (549 - _qr.topSkew || 0) * (canvasBoxStyle.height / 656)
};
radio = canvasBoxStyle.width / 344;
await drawIMG(
// 画二维码
_qr.url,
qrStyle.left,
qrStyle.top,
_qr.height * radio,
_qr.width * radio
);
Taro.hideLoading();
};
const copyPoster = async () => {
try {
props.shareDone({shareType: 'pc复制海报图片'});
// 将Canvas转换为Blob对象
const dataURL = canvas.toDataURL('image/png'); // 转换为dataURL
const blob = await (await fetch(dataURL)).blob(); // 将dataURL转为Blob
// 创建一个 ClipboardItem 对象
const item = new ClipboardItem({'image/png': blob});
// 复制到剪贴板
await navigator.clipboard.write([item]);
Taro.showToast({title: '复制成功', icon: 'success'});
emits('close');
} catch (err) {
console.error('复制失败:', err);
}
};
const downloadPoster = () => {
props.shareDone({shareType: 'pc下载海报图片'});
// 使用toDataURL方法将Canvas内容转换为dataURL
const dataURL = canvas.toDataURL('image/png'); // 默认为png格式,也可以指定其他格式如 "image/jpeg"
// 创建隐藏的可下载链接
const downloadLink = document.createElement('a');
downloadLink.href = dataURL;
downloadLink.download = 'share-image.png'; // 设置下载文件名
// 触发点击事件以开始下载
document.body.appendChild(downloadLink);
downloadLink.click();
// 下载后移除链接,防止页面上残留多个隐藏的链接
document.body.removeChild(downloadLink);
emits('close');
};
function drawIMG(src, x, y, width, height) {
return new Promise(resolve => {
let img = new Image();
img.src = src;
img.width = width;
img.height = height;
img.crossOrigin = 'anonymous';
img.onload = () => {
ctx.drawImage(img, x, y, width, height);
resolve(`[${src} ok]`);
};
});
}
onMounted(() => {
const SystemInfoSync = Taro.getSystemInfoSync();
const windowHeight = SystemInfoSync.windowHeight;
// const canvasHeight = windowHeight - 188 - 23 - 14;
const canvasHeight = windowHeight - 240;
const canvasWidth = canvasHeight * (344 / 656);
canvasBoxStyle.height = canvasHeight;
canvasBoxStyle.width = canvasWidth;
Taro.showLoading({title: '生成海报中...'});
setTimeout(() => {
initImage();
}, 500);
});
const close = () => emits('close');
defineExpose({close});
</script>
<style lang="scss">
.pc-save-image-popup {
height: 100%;
.poster-main-box {
height: calc(100% - 205px);
}
.poster-main {
margin: 0 auto;
}
.btn-box {
.icon-box {
width: 48px;
height: 48px;
background: #f9f9f9;
border-radius: 24px;
display: flex;
justify-content: center;
align-items: center;
}
.top-btn {
height: 120px;
margin: 0 12px;
border-radius: 12px;
background: white;
display: flex;
justify-content: space-around;
align-items: center;
.text {
font-size: 12px;
color: #555555;
line-height: 12px;
margin-top: 12px;
}
}
.bottom-btn {
height: 48px;
line-height: 48px;
background: #ffffff;
border-radius: 12px;
margin: 0 12px;
margin-top: 8px;
text-align: center;
color: #a5a5a5;
font-size: 16px;
}
}
}
</style>