Files

381 lines
9.2 KiB
Vue
Raw Permalink Normal View History

<!--
@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";
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,//这里是重点,获取实例的时候保存为全局变量就行了
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)
// #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
@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>
</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>