2024-02-08 21:01:37 +08:00
|
|
|
|
<!--
|
|
|
|
|
@name: GoodsPoster
|
|
|
|
|
@author: kahu4
|
|
|
|
|
@date: 2024-01-17 17:38
|
|
|
|
|
@description:商品海报
|
|
|
|
|
@update: 2024-01-17 17:38
|
|
|
|
|
-->
|
|
|
|
|
<script setup>
|
|
|
|
|
import { getCurrentInstance, nextTick, ref } from "vue";
|
|
|
|
|
import { useImage } from "@/hooks/useImage";
|
2024-02-22 18:37:23 +08:00
|
|
|
|
import { useMainStore } from "@/store/modules/useMainStore";
|
2024-02-08 21:01:37 +08:00
|
|
|
|
import { useShare } from "@/hooks/useShare";
|
|
|
|
|
import { generateMiniProgramQrCode } from "@/api/global";
|
|
|
|
|
|
|
|
|
|
// =================================== hooks ============================================
|
|
|
|
|
const mainStore = useMainStore();
|
|
|
|
|
const {getImageInfo, base64ToUrl, saveImageToPhotosAlbum} = useImage();
|
|
|
|
|
|
|
|
|
|
const {shareInfo, goodsDetailShare} = useShare();
|
|
|
|
|
|
|
|
|
|
const show = ref(false)
|
|
|
|
|
|
|
|
|
|
const goods = ref(undefined)
|
|
|
|
|
|
|
|
|
|
const miniProgramQrCode = ref('')
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 打开分享弹窗
|
|
|
|
|
* @param good 商品信息
|
|
|
|
|
*/
|
|
|
|
|
async function open(good) {
|
|
|
|
|
goods.value = good
|
|
|
|
|
show.value = true
|
|
|
|
|
// 生成小程序二维码
|
|
|
|
|
uni.showLoading({title: '获取数据中...'})
|
|
|
|
|
goodsDetailShare(good)
|
|
|
|
|
miniProgramQrCode.value = await generateMiniProgramQrCode({path: `pages/share/index`, name: shareInfo.value.query});
|
|
|
|
|
await nextTick(() => {
|
|
|
|
|
draw()
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 关闭分享弹窗
|
|
|
|
|
*/
|
|
|
|
|
function close() {
|
|
|
|
|
goods.value = posterImagePath.value = undefined
|
|
|
|
|
show.value = false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
defineExpose({open, close})
|
|
|
|
|
// ======================= 画布 =========================================
|
|
|
|
|
const posterImagePath = ref(null) // 海报图片路径
|
|
|
|
|
const _this = getCurrentInstance() // 当前组件实例
|
|
|
|
|
const canvasRef = ref() // 画布ref
|
|
|
|
|
const ctx = uni.createCanvasContext('goods-canvas', _this) // 画笔对象
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 绘制画布
|
|
|
|
|
*/
|
|
|
|
|
function draw() {
|
|
|
|
|
uni.showLoading({title: '海报生成中'})
|
|
|
|
|
const selectorQuery = uni.createSelectorQuery().in(_this);
|
|
|
|
|
selectorQuery.select('#goods-canvas').boundingClientRect().exec(async res => {
|
|
|
|
|
try {
|
|
|
|
|
const {width, height} = res[0] //px
|
|
|
|
|
// 绘制背景
|
|
|
|
|
drawRoundedRectangle({
|
|
|
|
|
width,
|
|
|
|
|
height,
|
|
|
|
|
round: 10
|
|
|
|
|
})
|
|
|
|
|
// 绘制header
|
|
|
|
|
await drawUserInfo()
|
|
|
|
|
// 绘制商品信息
|
|
|
|
|
await drawGoodsInfo(width)
|
|
|
|
|
// 绘制小程序分享信息
|
|
|
|
|
await drawShareInfo(width)
|
|
|
|
|
ctx.draw(false, () => {
|
|
|
|
|
generatePoster()
|
|
|
|
|
});
|
|
|
|
|
} catch (e) {
|
|
|
|
|
uni.hideLoading()
|
|
|
|
|
console.dir(e)
|
|
|
|
|
throw new Error(e)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 绘制用户信息
|
|
|
|
|
* @returns {Promise<void>}
|
|
|
|
|
*/
|
|
|
|
|
async function drawUserInfo() {
|
|
|
|
|
// 用户头像
|
|
|
|
|
await drawImage({x: 15, y: 15, width: 32, height: 32, src: mainStore.user.avatar})
|
|
|
|
|
ctx.save()
|
|
|
|
|
ctx.font = 'normal 16px sans-serif';
|
|
|
|
|
ctx.fillStyle = '#000000'
|
|
|
|
|
ctx.translate(68, 32)
|
|
|
|
|
// y 要设置成字体大小的一半
|
|
|
|
|
ctx.fillText(mainStore.user.nickname, 0, 8)
|
|
|
|
|
ctx.restore()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 绘制商品信息
|
|
|
|
|
* @returns {Promise<void>}
|
|
|
|
|
*/
|
|
|
|
|
async function drawGoodsInfo(width) {
|
|
|
|
|
// 图片
|
|
|
|
|
const imageHeight = width - 24 * 2 // 24是UI稿单边边距
|
|
|
|
|
const src = goods.value.image
|
|
|
|
|
await drawImage({x: 24, y: 72, width: imageHeight, height: imageHeight, src})
|
|
|
|
|
// 商品名称
|
|
|
|
|
const goodsName = goods.value.storeName
|
|
|
|
|
// 商品价格
|
|
|
|
|
const goodsPrice = goods.value.price
|
|
|
|
|
// 商品划线价格
|
|
|
|
|
const goodsOldPrice = goods.value.otPrice
|
|
|
|
|
|
|
|
|
|
const sliceName = ctx.measureText(goodsName) >= width / 1.5 ? goodsName : goodsName.slice(0, 17) + '...'
|
|
|
|
|
/** 商品名称 **/
|
|
|
|
|
ctx.save()
|
|
|
|
|
ctx.translate(24, width + 58)
|
|
|
|
|
ctx.font = 'normal bold 16px sans-serif';
|
|
|
|
|
ctx.fillStyle = '#000000'
|
|
|
|
|
ctx.fillText(sliceName, 0, 0)
|
|
|
|
|
ctx.restore()
|
|
|
|
|
/** 商品原始价格 **/
|
|
|
|
|
ctx.save()
|
|
|
|
|
ctx.translate(24, width + 58 + 36)
|
|
|
|
|
ctx.font = 'normal bold 24px sans-serif';
|
|
|
|
|
ctx.fillStyle = '#EE6D46'
|
|
|
|
|
ctx.fillText(`¥${ goodsPrice }`, 0, 0)
|
|
|
|
|
// 删除线价格
|
|
|
|
|
const goodsPriceTextMetrics = ctx.measureText(`¥${ goodsPrice }`); // 商品价格宽度
|
|
|
|
|
ctx.font = 'normal normal 16px sans-serif';
|
|
|
|
|
ctx.fillStyle = '#999999'
|
|
|
|
|
ctx.fillText(`¥${ goodsOldPrice }`, goodsPriceTextMetrics.width + 10, 0)
|
|
|
|
|
const goodsOldPriceTextMetrics = ctx.measureText(`¥${ goodsOldPrice }`); // 商品删除价格宽度
|
|
|
|
|
ctx.fillRect(goodsPriceTextMetrics.width + 10, -8, goodsOldPriceTextMetrics.width + 5, 1.5)
|
|
|
|
|
ctx.restore()
|
|
|
|
|
/** 分割线 */
|
|
|
|
|
ctx.save()
|
|
|
|
|
ctx.beginPath()
|
|
|
|
|
ctx.strokeStyle = '#F0F0F0'
|
|
|
|
|
ctx.translate(24, width + 58 + 36 + 15)
|
|
|
|
|
ctx.moveTo(0, 0)
|
|
|
|
|
ctx.lineWidth = 1
|
|
|
|
|
ctx.lineTo(imageHeight, 0)
|
|
|
|
|
ctx.stroke()
|
|
|
|
|
ctx.closePath()
|
|
|
|
|
ctx.restore()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 绘制分享信息
|
|
|
|
|
* @returns {Promise<void>}
|
|
|
|
|
*/
|
|
|
|
|
async function drawShareInfo(width) {
|
|
|
|
|
ctx.save();
|
|
|
|
|
ctx.translate(24, width + 58 + 36 + 15 + 25)
|
|
|
|
|
const text = "长按识别图中二维码"
|
|
|
|
|
const subText = "来自「Yshop商城」小程序"
|
|
|
|
|
ctx.font = 'normal bold 16px sans-serif';
|
|
|
|
|
ctx.fillStyle = '#000000'
|
|
|
|
|
// 主要字体
|
|
|
|
|
ctx.fillText(text, 0, 0)
|
|
|
|
|
ctx.font = 'normal normal 14px sans-serif';
|
|
|
|
|
ctx.fillStyle = '#8C8C8C'
|
|
|
|
|
ctx.fillText(subText, 0, 25)
|
|
|
|
|
const path = await base64ToUrl(miniProgramQrCode.value);
|
|
|
|
|
|
|
|
|
|
/** 二维码 **/
|
|
|
|
|
await drawImage({
|
|
|
|
|
x: width - (24 * 2) - 50,
|
|
|
|
|
y: -18,
|
|
|
|
|
width: 50,
|
|
|
|
|
height: 50,
|
|
|
|
|
src: path
|
|
|
|
|
})
|
|
|
|
|
ctx.restore();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 绘制圆角矩形
|
|
|
|
|
* @param options
|
|
|
|
|
*/
|
|
|
|
|
function drawRoundedRectangle(options = {}) {
|
|
|
|
|
ctx.save()
|
|
|
|
|
const defaultOptions = {
|
|
|
|
|
x: 0,
|
|
|
|
|
y: 0,
|
|
|
|
|
width: 0,
|
|
|
|
|
height: 0,
|
|
|
|
|
round: 0,
|
|
|
|
|
stroke: false,
|
|
|
|
|
strokeStyle: '#ffffff',
|
|
|
|
|
fillStyle: '#ffffff',
|
|
|
|
|
}
|
|
|
|
|
options = {...defaultOptions, ...options}
|
|
|
|
|
const {x, y, width, height, round, stroke, strokeStyle, fillStyle} = options
|
|
|
|
|
ctx.beginPath();
|
|
|
|
|
ctx.moveTo(x + round, y);
|
|
|
|
|
ctx.arcTo(x + width, y, x + width, y + round, round);
|
|
|
|
|
ctx.lineTo(x + width, y + height - round);
|
|
|
|
|
ctx.arcTo(x + width, y + height, x + width - round, y + height, round);
|
|
|
|
|
ctx.lineTo(x + round, y + height);
|
|
|
|
|
ctx.arcTo(x, y + height, x, y + height - round, round);
|
|
|
|
|
ctx.lineTo(x, y + round);
|
|
|
|
|
ctx.arcTo(x, y, x + round, y, round);
|
|
|
|
|
ctx.closePath();
|
|
|
|
|
ctx.fillStyle = fillStyle;
|
|
|
|
|
ctx.strokeStyle = strokeStyle;
|
|
|
|
|
ctx.fill();
|
|
|
|
|
stroke && ctx.stroke()
|
|
|
|
|
ctx.restore()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 绘制图片
|
|
|
|
|
* @param options
|
|
|
|
|
* @returns {Promise<void>}
|
|
|
|
|
*/
|
|
|
|
|
async function drawImage(options = {}) {
|
|
|
|
|
const defaultOptions = {
|
|
|
|
|
x: 0,
|
|
|
|
|
y: 0,
|
|
|
|
|
width: 0,
|
|
|
|
|
height: 0,
|
|
|
|
|
src: ''
|
|
|
|
|
}
|
|
|
|
|
options = {...defaultOptions, ...options}
|
|
|
|
|
// 获取图片信息
|
|
|
|
|
const {path} = await getImageInfo(options.src);
|
|
|
|
|
ctx.drawImage(path, options.x, options.y, options.width, options.height)
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 生成画布
|
|
|
|
|
* @returns {Promise<void>}
|
|
|
|
|
*/
|
|
|
|
|
async function generatePoster() {
|
|
|
|
|
console.log("开始生成画布")
|
|
|
|
|
// #ifndef MP-WEIXIN
|
|
|
|
|
uni.canvasToTempFilePath({
|
|
|
|
|
canvasId: 'goods-canvas',
|
|
|
|
|
success: async (res) => {
|
|
|
|
|
// 在H5平台下,tempFilePath 为 base64
|
|
|
|
|
// #ifdef H5
|
|
|
|
|
posterImagePath.value = await base64ToUrl(res.tempFilePath);
|
|
|
|
|
// #endif
|
|
|
|
|
// #ifndef H5
|
|
|
|
|
posterImagePath.value = res.tempFilePath;
|
|
|
|
|
console.log(posterImagePath.value, res)
|
|
|
|
|
// #endif
|
|
|
|
|
uni.hideLoading()
|
|
|
|
|
},
|
|
|
|
|
fail: (err) => {
|
|
|
|
|
console.log(err)
|
|
|
|
|
uni.hideLoading()
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
// #endif
|
|
|
|
|
// #ifdef MP-WEIXIN
|
|
|
|
|
wx.canvasToTempFilePath({
|
|
|
|
|
canvasId: 'goods-canvas',
|
2024-02-22 18:37:23 +08:00
|
|
|
|
canvas: canvasRef.value,//这里是重点,获取实例的时候保存为全局变量就行了
|
2024-02-08 21:01:37 +08:00
|
|
|
|
success: async (res) => {
|
|
|
|
|
posterImagePath.value = res.tempFilePath;
|
|
|
|
|
uni.hideLoading()
|
|
|
|
|
},
|
|
|
|
|
fail: (err) => {
|
|
|
|
|
console.log(err)
|
|
|
|
|
uni.hideLoading()
|
|
|
|
|
}
|
2024-02-22 18:37:23 +08:00
|
|
|
|
}, _this)
|
2024-02-08 21:01:37 +08:00
|
|
|
|
// #endif
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 保存到相册
|
|
|
|
|
* @returns {Promise<void>}
|
|
|
|
|
*/
|
|
|
|
|
async function save() {
|
|
|
|
|
uni.showLoading({title: '保存中'})
|
|
|
|
|
try {
|
|
|
|
|
await saveImageToPhotosAlbum(posterImagePath.value, goods.value.storeName)
|
|
|
|
|
close()
|
|
|
|
|
} finally {
|
|
|
|
|
uni.hideLoading()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<template>
|
2024-02-22 18:37:23 +08:00
|
|
|
|
<uv-overlay
|
2024-02-08 21:01:37 +08:00
|
|
|
|
@touchmove="(e)=>{e.preventDefault();}"
|
|
|
|
|
:show="show"
|
|
|
|
|
@click="close">
|
|
|
|
|
<view
|
|
|
|
|
class="poster"
|
|
|
|
|
v-if="goods && posterImagePath">
|
|
|
|
|
<!-- 海报 -->
|
|
|
|
|
<view
|
|
|
|
|
class="poster-image"
|
|
|
|
|
@click.stop>
|
|
|
|
|
<image :src="posterImagePath" />
|
|
|
|
|
</view>
|
|
|
|
|
<!-- 按钮组合 -->
|
|
|
|
|
<view class="button-group">
|
|
|
|
|
<view
|
|
|
|
|
class="button line-button"
|
|
|
|
|
@click.stop="close">
|
|
|
|
|
取消
|
|
|
|
|
</view>
|
|
|
|
|
<view
|
|
|
|
|
class="button animation-button"
|
|
|
|
|
@click.stop="save">
|
|
|
|
|
保存
|
|
|
|
|
</view>
|
|
|
|
|
</view>
|
|
|
|
|
</view>
|
|
|
|
|
<canvas
|
|
|
|
|
canvas-id="goods-canvas"
|
|
|
|
|
ref="canvasRef"
|
|
|
|
|
style="width:654rpx;height: 1032rpx;margin: 20rpx auto;position: absolute;z-index:999;top: -999px;"
|
|
|
|
|
id="goods-canvas" />
|
|
|
|
|
|
2024-02-22 18:37:23 +08:00
|
|
|
|
</uv-overlay>
|
2024-02-08 21:01:37 +08:00
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<style
|
|
|
|
|
scoped
|
|
|
|
|
lang="scss">
|
|
|
|
|
.poster {
|
|
|
|
|
width: 100%;
|
|
|
|
|
height: 100%;
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
|
|
|
|
.poster-image {
|
|
|
|
|
width: 90%;
|
|
|
|
|
aspect-ratio: 327 / 516;
|
|
|
|
|
|
|
|
|
|
image {
|
|
|
|
|
width: 100%;
|
|
|
|
|
height: 100%;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.button-group {
|
|
|
|
|
width: 90%;
|
|
|
|
|
margin-top: 40rpx;
|
|
|
|
|
@include useFlex(center, center, row, nowrap, 20rpx);
|
|
|
|
|
|
|
|
|
|
.button {
|
|
|
|
|
flex-grow: 1;
|
|
|
|
|
@include useFlex(center, center);
|
|
|
|
|
height: 80rpx;
|
|
|
|
|
background: $primary-color;
|
|
|
|
|
color: $white-color;
|
|
|
|
|
border-radius: 10rpx;
|
|
|
|
|
box-sizing: border-box;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.line-button {
|
|
|
|
|
border: 4rpx solid $white-color;
|
|
|
|
|
background: transparent;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
</style>
|