Files
2024-02-22 18:37:23 +08:00

381 lines
9.2 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!--
@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";
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',
canvas: canvasRef.value,//这里是重点,获取实例的时候保存为全局变量就行了
success: async (res) => {
posterImagePath.value = res.tempFilePath;
uni.hideLoading()
},
fail: (err) => {
console.log(err)
uni.hideLoading()
}
}, _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>
<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" />
</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>