381 lines
9.1 KiB
Vue
381 lines
9.1 KiB
Vue
<!--
|
||
@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/store";
|
||
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>
|
||
<u-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" />
|
||
|
||
</u-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>
|