写100个前端效率工具(1):uni-app海报生成 uni-wxml2canvas

开篇

最近自己觉得很迷茫,没有什么技术热情,不知道该做些什么,就写写效率工具,并记录下来吧。

后面我会不定期写,一天1个、两天1个、三天1个甚至一周1个,都有可能。

✨ 工具介绍

你是否在uni-app开发中遇到过这样的需求:需要将复杂的WXML结构转换为精美的海报图片?今天给大家推荐一款高效工具,让你轻松实现WXML到Canvas的转换!

uni-wxml2canvas 是一个声明式配置驱动的uni-app工具库,它能将WXML结构通过Canvas渲染并导出为图片。特别适合生成分享海报、商品卡片等需要动态绘制的场景。

核心优势

  • 开箱即用,API简洁易上手
  • 高性能渲染,支持多层叠加
  • 支持多种元素类型(图片/文本/圆形/矩形/圆角矩形)
  • 完美适配H5,微信小程序,支付宝小程序,百度小程序,字节跳动小程序,QQ小程序

📱平台兼容

注意:比如微信小程序需要配置 downloadFile 合法域名

H5 微信小程序 支付宝小程序 百度小程序 字节跳动小程序 QQ 小程序

📦 安装指南

选择你喜欢的包管理器安装:

bash 复制代码
# npm
npm install uni-wxml2canvas@latest

# yarn
yarn add uni-wxml2canvas@latest

# pnpm
pnpm add uni-wxml2canvas@latest

🛠️ 配置详细

canvasId:canvas-id的值

html 复制代码
<template>
  <canvas
    canvas-id="your canvasId"
    style="position: absolute; top: -99999rpx; width: 750rpx; height: 1334rpx; opacity: 0;"
  />
</template>

options:描述最终要生成的图片中存在哪些内容

typescript 复制代码
export type W2CDrawType = 'IMAGE' | 'TEXT' | 'CIRCLE' | 'RECT' | 'ROUNDRECT'

export type W2COptions = Record<'width' | 'height', number> & Record<'wxml', W2CWxml | W2CWxml[]>

/** wxml 描述 */
export interface W2CWxml {
  type: W2CDrawType
  layer?: number // 层级控制,默认 1
  desc: // 类型专属配置
    | W2CDrawImage
    | W2CDrawText
    | W2CDrawCircle
    | W2CDrawRect
    | W2CDrawRoundRect
}

/** 绘制的起点、终点 */
export interface W2CDrawStartPosition {
  startX: number
  startY: number
}

layer

layer 是一个很重要的参数,可以用于控制层级。比如说: 第一种情况:绘制海报时,需要先渲染背景图,那么此时可以将其他 wxml 的 layer 值调大,绘制出来的效果就是背景图在下而其他在上;

第二种情况:绘制进度条时,需要先绘制进度条再绘制当前进度,那么就可以将当前进度的 layer 值调大,绘制处理的效果就是当前进度在上了。

type

type 分为五种类型,分别是:

元素类型 关键配置项 说明
IMAGE url 图片URL
TEXT text, font, align 支持多行文
CIRCLE radius 圆形半径
RECT width, height 矩形宽高
ROUNDRECT radius 圆角半径
IMAGE 类型

用来绘制图片

typescript 复制代码
export interface W2CDrawImage extends W2CDrawStartPosition {
  url: string
}
TEXT 类型

用来绘制文本,支持多行文本控制(maxWidth、lineHeight)

typescript 复制代码
export interface W2CDrawText extends W2CDrawStartPosition {
  text: string
  font?: string
  align?: 'left' | 'center' | 'right'
  /** 支持 rgb、rgba、十六进制、颜色英文 */
  fillColor?: string
  maxWidth?: number
  lineHeight?: number
}
CIRCLE 类型

用来绘制圆,fillColor 用于控制填充色,lineWidhtlineColor 用于控制是否描边

typescript 复制代码
export interface W2CDrawCircle extends W2CDrawStartPosition {
  radius: number
  /** 支持 rgb、rgba、十六进制、颜色英文 */
  fillColor?: string
  lineWidth?: number
  lineColor?: string
}
RECT 类型

用来绘制矩形

typescript 复制代码
export interface W2CDrawRect extends W2CDrawStartPosition {
  width: number
  height: number
  /** 支持 rgb、rgba、十六进制、颜色英文 */
  fillColor?: string
  lineWidth?: number
  lineColor?: string
}
ROUNDRECT 类型

用来绘制圆角矩形

typescript 复制代码
export interface W2CDrawRoundRect extends W2CDrawRect {
  radius: number
}

💡 来个实际例子

代码(去掉了实际图片)

html 复制代码
<template>
  <view class="relative w-459rpx h-73rpx overflow-hidden">
    <button
      class="w-full h-70rpx flex-cc font-ss-h text-30rpx text-[#a14101]" 
      @click="onGenerate"
    >
      生成海报
    </button>
    <canvas
      canvas-id="poster-canvas"
      class="absolute top--99999rpx w-648px h-1016px opacity-0"
    ></canvas>
  </view>

  <uni-popup ref="PosterRef" mask-background-color="rgba(0, 0, 0, 0.6)" @change="">
    <view class="relative">
      <image
        :src="posterUrl"
        mode="aspectFit"
        show-menu-by-longpress
        class="w-648rpx h-1016rpx"
      ></image>
    </view>
  </uni-popup>
</template>

<script lang="ts" setup>
import { Wxml2Canvas, type W2CWxml } from '@tttiga/uni-wxml2canvas'

const PosterRef = ref()

const posterUrl = ref('')

const current = computed(() => 200)

const calculateNonLinearProgress = (currentValue: number, totalValue: number): number => {
  if (currentValue === 0) return 0

  if (currentValue >= totalValue) return 1

  const startPercent = 1 / 9 // 约11.11%
  const halfPercent = 0.5 // 50%
  const endPercent = 8 / 9 // 约88.89%

  if (currentValue <= 200) {
    // 到达 200,占 1/9
    if (currentValue <= 30) {
      return (30 / 200) * startPercent
    }
    return (currentValue / 200) * startPercent
  }

  if (currentValue <= 400) {
    // 从 200 到 400 的进度是从 1/9 到 1/2
    const ratio = (currentValue - 200) / 200
    return startPercent + ratio * (halfPercent - startPercent)
  }

  if (currentValue <= 600) {
    // 从 400 到 600 的进度是从 1/2 到 8/9
    const ratio = (currentValue - 400) / 200
    return halfPercent + ratio * (endPercent - halfPercent)
  }

  // 从 600 到 800 的进度是从 8/9 到 100%
  const ratio = (currentValue - 600) / 200
  return endPercent + ratio * (1 - endPercent)
}

const currentProgress = computed(() => {
  return calculateNonLinearProgress(current.value, 800)
})

const generatePoster = async () => {
  uni.showLoading({ title: '海报生成中...' })

  try {
    const texts: W2CWxml[] = [
      {
        type: 'TEXT',
        desc: {
          text: '我是大标题',
          startX: 327,
          startY: 124,
          font: `34px SourceHanSerifCN-Heavy`,
          align: 'center',
          fillColor: '#403d38',
        },
      },
      {
        type: 'TEXT',
        desc: {
          text: '我是小标题',
          startX: 327,
          startY: 162,
          font: `22px SourceHanSerifCN-SemiBold`,
          align: 'center',
          fillColor: '#a38d7d',
        },
      },
      {
        type: 'TEXT',
        desc: {
          text: '我还是小标题',
          startX: 330,
          startY: 237,
          font: `24px SourceHanSerifCN-Heavy`,
          align: 'center',
          fillColor: '#a14101',
        },
      },
      {
        type: 'TEXT',
        desc: {
          text: '快点来打卡吧~',
          startX: 63,
          startY: 874,
          font: `30px SourceHanSerifCN-Heavy`,
          align: 'left',
          fillColor: '#403d38',
        },
      },
      {
        type: 'TEXT',
        desc: {
          text: '扫码看看',
          startX: 63,
          startY: 912,
          font: `24px SourceHanSerifCN-SemiBold`,
          align: 'left',
          fillColor: '#a38d7d',
        },
      },
    ]

    const progress: W2CWxml = {
      type: 'ROUNDRECT',
      desc: {
        startX: 72,
        startY: 422,
        width: 500,
        height: 14,
        radius: 7,
        fillColor: '#a38d7d',
      },
    }

    const completedWidth = Math.min(currentProgress.value * 500, 500)
    const progressBar: W2CWxml = {
      type: 'ROUNDRECT',
      desc: {
        startX: 72,
        startY: 422,
        width: completedWidth,
        height: 14,
        radius: completedWidth ? 7 : 0,
        fillColor: '#fac13e',
      },
    }

    const milestones: W2CWxml[] = [
      { score: 200, label: '普通奖池', image: 'https://xxx.png', image2: 'https://xxx.png' },
      { score: 400, label: '中级奖池', image: 'https://xxx.png', image2: 'https://xxx.png' },
      { score: 600, label: '高级奖池', image: 'https://xxx.png', image2: 'https://xxx.png' },
    ]
      .map((item) => {
        // 计算里程碑位置
        const milestoneX =
          72 + Math.max(7, Math.min(500 - 7, calculateNonLinearProgress(item.score, 800) * 500))

        const isUnlocked = current.value < item.score
        const image = !isUnlocked ? item.image2 : item.image
        const imageWidth = 49
        const imageHeight = 47
        return [
          {
            type: 'IMAGE',
            layer: 3,
            desc: {
              url: image,
              startX: milestoneX - imageWidth / 2,
              startY: 429 - imageHeight / 2 - (!isUnlocked ? 8 : 0),
            },
          },
          {
            type: 'CIRCLE',
            layer: 2,
            desc: {
              startX: milestoneX,
              startY: 429,
              radius: 26,
              fillColor: !isUnlocked ? '#fac13e' : '#a38d7d',
              lineWidth: 0,
              lineColor: 'rgba(0, 0, 0, 0)',
            },
          },
          {
            type: 'TEXT',
            desc: {
              text: `${item.score}积分`,
              startX: milestoneX,
              startY: 396,
              font: `18px SourceHanSerifCN-Bold`,
              align: 'center',
              fillColor: '#403d38',
            },
          },
          {
            type: 'TEXT',
            desc: {
              text: item.label,
              startX: milestoneX,
              startY: 476,
              font: `18px SourceHanSerifCN-Bold`,
              align: 'center',
              fillColor: '#403d38',
            },
          },
        ] as W2CWxml[]
      })
      .flat(2)

    const wxml2canvas = new Wxml2Canvas('poster-canvas', {
      width: 648,
      height: 1016,
      wxml: [
        {
          type: 'IMAGE',
          layer: 0,
          desc: {
            url: 'https://xxxx.png',
            startX: 0,
            startY: 0,
          },
        },
        ...texts,
        progress,
        progressBar,
        ...milestones,
      ],
    })

    posterUrl.value = await wxml2canvas.generate()
  } catch (e) {
    console.error('海报生成失败', e)
    uni.showToast({
      title: '海报生成失败',
      icon: 'none',
    })
  } finally {
    uni.hideLoading()
  }
}

const onGenerate = async () => {
  await generatePoster()
  PosterRef.value?.open?.()
}
</script>

效果

相关推荐
雪芽蓝域zzs2 小时前
uni-app 将 base64 图片编码转为 Blob 本地文件路径
网络协议·udp·uni-app
星光一影5 小时前
陪诊陪检系统源码,陪诊小程序,陪诊APP,陪诊服务,家政上门系统,居家护理陪护源码
mysql·小程序·uni-app·php
Qlittleboy6 小时前
uniapp里 rich-text 里的img图片如何控制最大宽度
uni-app
阿奇__6 小时前
uniapp h5 app 小程序获取当前定位
小程序·uni-app
*小雪6 小时前
uniapp写H5授权登录及分享,返回到目标页面
开发语言·javascript·uni-app
一只程序熊6 小时前
uniapp uniim ios配置消息推送
uni-app
2501_915106329 小时前
iOS性能调优的系统化实践,从架构分层到多工具协同的全流程优化指南(开发者深度版)
android·ios·小程序·架构·uni-app·iphone·webview
TE-茶叶蛋1 天前
Uniapp运行MuMu模拟器
uni-app
一人一程温一壶酒1 天前
微信小程序uniapp开发附源码——图片加水印
微信小程序·uni-app·notepad++
一个假的前端男1 天前
uniapp 3端轮播
前端·javascript·uni-app