uniapp实现生成海报功能 (开箱即用)

前言:该插件无需手写canvas所有写法都是HTML就可以轻松实现海报

在一些项目中有些需求会需要分享海报的功能,今天呢就为大家分享一下我在用uniapp开发中使用的一款插件,可以任意的布局。

插件市场地址:https://ext.dcloud.net.cn/plugin?id=22860

附带图片

效果图

步骤一:l-painter是边界容器 这个里面就是构建海报的区域

javascript 复制代码
   <!-- 最好是用position移除屏幕外,使用show、opacity或者visibility可能会出现别的问题  -->
<l-painter v-if="detail && isPainting" ref="painter" @done="onPainterDone" @fail="onPainterFail" style="position: absolute;left: -1000%;top: -1000%;"></l-painter>

onPainterDone:是海报生成成功的回调

onPainterFail:是海报生成失败的回调

l-painter-view:是块级容器类似div和view

l-painter-text:是类似p标签和text用于书写文字

l-painter-image:是image标签和img标签

基本用法:

创建一个新的文件Poster.vue 我已经进行过一个初步的封装 传入需要渲染的对应静态数据即可

父组件使用

javascript 复制代码
<!-- 分享游戏 -->
<Poster ref="posterRef" :gameInfo="gameTempInfo" />
import Poster from "@/components/Poster.vue";

HTML结构:简单试试在画板上布局排版自己想要的效果,下面代码是我效果图的结构

javascript 复制代码
 <l-painter v-if="detail && isPainting" ref="painter" @done="onPainterDone" @fail="onPainterFail"
        style="position: absolute;left: -1000%;top: -1000%;">
        <l-painter-view
            :css="`position: relative;width: 600rpx;height: 800rpx;object-fit: cover;background-image: url(替换成你的背景图片地址);`">
            <l-painter-view
                :css="`width: 536rpx;height: 536rpx;border-radius: 32rpx;background: rgba(217, 217, 217, 1);margin: 32rpx;background-size: cover;background-image: url(${detail.coverUrl});`">
                <l-painter-view
                    :css="`width: 148rpx;height: 44rpx;background: rgba(0, 0, 0, 0.60);line-height: 44rpx;text-align: center;position: absolute;top: 12rpx;left: 12rpx;border-radius: 28rpx;`">
                    <l-painter-text :text="detail.tag"
                        css="font-size: 24rpx;color: rgba(255, 213, 0, 1)"></l-painter-text>
                </l-painter-view>
                <l-painter-view
                    :css="`height: 123rpx;width: 504rpx;border-radius: 0 0 32rpx 32rpx;position: absolute;bottom: 0;left: 0;background: linear-gradient(0deg, rgba(0,0,0,0.8) 0%, rgba(0,0,0,0) 100%);padding-top: 45rpx;padding-left: 32rpx;`">
                    <l-painter-text :text="detail.gameTitle"
                        css="font-size: 32rpx;color: rgba(255, 255, 255, 1);margin-bottom: 12rpx;"></l-painter-text>
                    <l-painter-view css="display: flex;">
                        <l-painter-image
                            css="width: 32rpx;height: 32rpx;border-radius: 50%;object-fit: cover;margin-right: 12rpx;"
                            :src="detail.avatarUrl"></l-painter-image>
                        <l-painter-text css="font-size: 24rpx;color: rgba(255, 255, 255, 0.50);"
                            :text="detail.nickName"></l-painter-text>
                    </l-painter-view>
                </l-painter-view>
            </l-painter-view>
            <l-painter-image
                css="width: 170rpx;height: 170rpx;object-fit: cover;margin-right: 12rpx;position: absolute;bottom: 32rpx;right: 32rpx;"
                :src="detail.code"></l-painter-image>
        </l-painter-view>
    </l-painter>

JS文件需要进行一定的数据替换

javascript 复制代码
<script setup>
import { ref, computed } from 'vue';

// 渲染参数
const props = defineProps({
    gameInfo: {
        type: Object,
        default: () => ({})
    }
});

// 海报的DOM实例
const painter = ref(null);
// 是否显示
const visible = ref(false);
// 是否在生成中
const isPainting = ref(false);
// 海报地址
const imgSrc = ref('');
// 渲染参数
const detail = ref(null);
// 定时器ID
let timeoutTimer = null;

// 打开弹框
const open = () => {
    // 渲染所用数据装配,合并自游戏详情与当前用户态
    detail.value = {
        tag: props.gameInfo?.gameType || '游戏海报',
        gameTitle: props.gameInfo?.gameTitle || '未命名游戏',
        avatarUrl: props.gameInfo?.authorAvatar || '默认地址',
        nickName: props.gameInfo?.authorNickname|| '探险者',
        coverUrl: props.gameInfo?.gameCoverUrl || '默认地址,
        code: props.gameInfo?.miniQrCode
    };

    imgSrc.value = '';
    visible.value = true;
    renderPoster();
};

// 数据处理
const renderPoster = () => {
    imgSrc.value = '';
    isPainting.value = true;
    clearTimeout(timeoutTimer);
    timeoutTimer = setTimeout(() => {
        if (isPainting.value && !imgSrc.value) {
            isPainting.value = false;
            uni.showToast({ title: '海报生成超时', icon: 'none' });
        }
    }, 15000);
};

// 生成海报
const onPainterDone = () => {
    setTimeout(() => {
        if (painter.value) {
            painter.value.canvasToTempFilePathSync({
                fileType: "jpg",
                pathType: 'url',
                quality: 1,
                success: (res) => {
                    imgSrc.value = res.tempFilePath;
                    isPainting.value = false;
                    clearTimeout(timeoutTimer);
                    wx.showShareImageMenu({
                        path: imgSrc.value,
                        success: (res) => {
                            console.log('分享图片菜单成功', res);
                        },
                        fail: (err) => {
                            console.error('分享图片菜单失败', err);
                        }
                    });
                },
                fail: (err) => {
                    console.error('取图失败', err);
                    isPainting.value = false;
                }
            });
        }
    }, 300);
};

// 失败回调
const onPainterFail = (err) => {
    console.error('l-painter fail', err);
    isPainting.value = false;
    clearTimeout(timeoutTimer);
};

// 暴露方法
defineExpose({ open });
</script>

CSS就是当前文件的一个背景配置了

css 复制代码
<style lang="scss" scoped>
.poster-container {
    width: 100vw;
    height: 100vh;
    position: absolute;
    top: 0;
    left: 0;
    z-index: 100;
}


.poster-card {
    width: 600rpx;
    height: 800rpx;
    border-radius: 32rpx;
    box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.3);
    background-color: transparent;
    position: relative;
    margin-top: 100rpx;

    .close-btn {
        width: 48rpx;
        height: 48rpx;
        position: absolute;
        top: -80rpx;
        right: 0;
    }


}

.poster-img {
    width: 100%;
    height: 100%;
    display: block;
    border-radius: 32rpx;
}

.poster-placeholder {
    width: 100%;
    height: 100%;
    background-color: #ffffff;
    border: 12rpx solid #16CABB;
    border-radius: 32rpx;
    box-sizing: border-box;
    display: flex;
    align-items: center;
    justify-content: center;

    .loading-text {
        font-size: 28rpx;
        color: #666;
    }
}

.action-buttons {
    margin-top: 40rpx;
    display: flex;
    flex-direction: row;
    justify-content: space-between;
    width: 600rpx;

    .btn {
        width: 280rpx;
        height: 88rpx;
        border-radius: 44rpx;
        font-size: 30rpx;
        font-weight: bold;
        display: flex;
        align-items: center;
        justify-content: center;
        color: #ffffff;
        box-sizing: border-box;
        padding: 0;
        margin: 0;

        &::after {
            border: none;
        }
    }

    .btn-friends {
        background-color: #16CABB;
        /* 主题青色 */
    }

    .btn-moment {
        background-color: #16CABB;
        /* 原型图中左右按钮颜色一致 */
    }
}

.painter-offscreen {
    position: fixed;
    left: -9999px;
    top: 0;
}
</style>
相关推荐
OpenTiny社区2 小时前
TinyRobot Skills技巧大公开:让 AI 成为你的 “UI 搭建”副驾驶
前端·vue.js·ai编程
Moment2 小时前
2026年,TypeScript还值不值得学 ❓❓❓
前端·javascript·面试
奶糖 肥晨2 小时前
微信小程序定位功能开发实战:实现自动定位、城市切换与地图导航
微信小程序·小程序
乌拉那拉丹2 小时前
vue3 配置跨域 (vite.config.ts中配置)
前端·vue.js
angerdream2 小时前
最新版vue3+TypeScript开发入门到实战教程之DOM操作
javascript·vue.js
笨笨狗吞噬者2 小时前
【uniapp】小程序支持分包引用分包 node_modules 依赖产物打包到分包中
前端·微信小程序·uni-app
Lee川3 小时前
Vue Router 4 核心精讲:从原理到面试实战
前端·vue.js
A923A3 小时前
【Vue3大事件 | 项目笔记】第六天
vue.js·笔记·前端框架·前端项目
我命由我123453 小时前
JS 开发问题:url.includes is not a function
开发语言·前端·javascript·html·ecmascript·html5·js