瀏覽代碼

海报:移除 qs-canvas 库,采用社区活跃的 lime-painter 来实现海报生成

puhui999 11 月之前
父節點
當前提交
8963ebc6af

+ 1 - 1
.env

@@ -5,7 +5,7 @@ SHOPRO_VERSION = v1.8.3
 SHOPRO_BASE_URL = http://api-dashboard.yudao.iocoder.cn
 
 # 后端接口 - 测试环境(通过 process.env.NODE_ENV = development)
-SHOPRO_DEV_BASE_URL = https://192.168.2.21:48080
+SHOPRO_DEV_BASE_URL = https://192.168.1.105:48080
 ### SHOPRO_DEV_BASE_URL = http://yunai.natapp1.cc
 
 # 后端接口前缀(一般不建议调整)

+ 0 - 1
package.json

@@ -94,7 +94,6 @@
     "luch-request": "^3.0.8",
     "pinia": "^2.0.33",
     "pinia-plugin-persist-uni": "^1.2.0",
-    "qs-canvas": "^1.0.11",
     "weixin-js-sdk": "^1.6.0"
   },
   "devDependencies": {

+ 25 - 57
sheep/components/s-share-modal/canvas-poster/index.vue

@@ -2,56 +2,17 @@
 <template>
   <su-popup :show="show" round="10" @close="onClosePoster" type="center" class="popup-box">
     <view class="ss-flex-col ss-col-center ss-row-center">
-      <view
-        v-if="poster.src === ''"
-        class="poster-title ss-flex ss-row-center"
-        :style="{
-          height: poster.height + 'px',
-          width: poster.width + 'px',
-        }"
-      >
-        海报加载中...
-      </view>
-      <image
-        v-else
-        class="poster-img"
-        :src="poster.src"
-        :style="{
-          height: poster.height + 'px',
-          width: poster.width + 'px',
-        }"
-        :show-menu-by-longpress="true"
-      />
-      <canvas
-        class="hideCanvas"
-        :canvas-id="poster.canvasId"
-        :id="poster.canvasId"
-        :style="{
-          height: poster.height + 'px',
-          width: poster.width + 'px',
-        }"
-      />
-      <view
-        class="poster-btn-box ss-m-t-20 ss-flex ss-row-between ss-col-center"
-        v-if="poster.src !== ''"
-      >
-        <button class="cancel-btn ss-reset-button" @tap="onClosePoster">取消</button>
-        <button class="save-btn ss-reset-button ui-BG-Main" @tap="onSavePoster">
-          {{
-            ['wechatOfficialAccount', 'H5'].includes(sheep.$platform.name)
-              ? '长按图片保存'
-              : '保存图片'
-          }}
-        </button>
-      </view>
+      <template v-if="poster.views.length > 0">
+        <l-painter :board="poster" />
+      </template>
     </view>
   </su-popup>
 </template>
 
 <script setup>
-  import { reactive, getCurrentInstance } from 'vue';
+  import { getCurrentInstance, reactive } from 'vue';
   import sheep from '@/sheep';
-  import useCanvas from './useCanvas';
+  import { getPosterData } from '@/sheep/components/s-share-modal/canvas-poster/poster';
 
   const props = defineProps({
     show: {
@@ -60,17 +21,22 @@
     },
     shareInfo: {
       type: Object,
-      default() {},
+      default() {
+      },
     },
   });
 
+
   const poster = reactive({
-    canvasId: 'canvasId',
-    width: sheep.$platform.device.windowWidth * 0.9,
-    height: 600,
-    src: '',
+    css: {
+      // 根节点若无尺寸,自动获取父级节点
+      width: sheep.$platform.device.windowWidth * 0.9,
+      height: 600,
+    },
+    views: [],
   });
 
+
   const emits = defineEmits(['success', 'close']);
   const vm = getCurrentInstance();
 
@@ -100,14 +66,11 @@
 
   // 使用 canvas 生成海报
   async function getPoster(params) {
-    poster.src = '';
-    poster.shareInfo = props.shareInfo;
-    // #ifdef APP-PLUS
-    poster.canvasId = 'canvasId-' + new Date().getTime();
-    // #endif
-    const canvas = await useCanvas(poster, vm);
-    console.log('canvas',canvas);
-    return canvas;
+    let drawer = await getPosterData({
+      width: sheep.$platform.device.windowWidth * 0.9,
+      shareInfo: props.shareInfo,
+    });
+    poster.views = drawer;
   }
 
   defineExpose({
@@ -119,9 +82,11 @@
   .popup-box {
     position: relative;
   }
+
   .poster-title {
     color: #999;
   }
+
   // 分享海报
   .poster-btn-box {
     width: 600rpx;
@@ -129,6 +94,7 @@
     left: 50%;
     transform: translateX(-50%);
     bottom: -80rpx;
+
     .cancel-btn {
       width: 240rpx;
       height: 70rpx;
@@ -139,6 +105,7 @@
       font-weight: 500;
       color: $dark-9;
     }
+
     .save-btn {
       width: 240rpx;
       height: 70rpx;
@@ -152,6 +119,7 @@
   .poster-img {
     border-radius: 20rpx;
   }
+
   .hideCanvas {
     position: fixed;
     top: -99999rpx;

+ 102 - 97
sheep/components/s-share-modal/canvas-poster/poster/goods.js

@@ -1,122 +1,127 @@
 import sheep from '@/sheep';
 import third from '@/sheep/api/migration/third';
-import { formatImageUrlProtocol, getBase64Src } from './index';
+import { formatImageUrlProtocol } from './index';
 
 const goods = async (poster) => {
   const width = poster.width;
   const userInfo = sheep.$store('user').userInfo;
   const wxa_qrcode = (await third.wechat.getWxacode(poster.shareInfo.path, poster.shareInfo.query)).data;
-  return {
-    background: formatImageUrlProtocol(sheep.$url.cdn(sheep.$store('app').platform.share.posterInfo.goods_bg)),
-    list: [
-      {
-        name: 'nickname',
-        type: 'text',
-        val: userInfo.nickname,
-        x: width * 0.22,
-        y: width * 0.06,
-        paintbrushProps: {
-          fillStyle: '#333',
-          font: {
-            fontSize: 16,
-            fontFamily: 'sans-serif',
-          },
-        },
+  return [
+    {
+      type: 'image',
+      src: formatImageUrlProtocol(sheep.$url.cdn(sheep.$store('app').platform.share.posterInfo.goods_bg)),
+      css: {
+        width,
+        background: 'white',
+        position: 'fixed',
+        'object-fit': 'contain',
+        top: '0',
+        left: '0',
+        zIndex: -1,
       },
-      {
-        name: 'avatar',
-        type: 'image',
-        val: formatImageUrlProtocol(sheep.$url.cdn(userInfo.avatar)),
-        x: width * 0.04,
-        y: width * 0.04,
+    },
+    {
+      type: 'text',
+      text: userInfo.nickname,
+      css: {
+        color: '#333',
+        fontSize: 16,
+        fontFamily: 'sans-serif',
+        position: 'fixed',
+        top: width * 0.06,
+        left: width * 0.22,
+      },
+    },
+    {
+      type: 'image',
+      src: formatImageUrlProtocol(sheep.$url.cdn(userInfo.avatar)),
+      css: {
+        position: 'fixed',
+        left: width * 0.04,
+        top: width * 0.04,
         width: width * 0.14,
         height: width * 0.14,
-        d: width * 0.14,
       },
-      {
-        name: 'goodsImage',
-        type: 'image',
-        val: formatImageUrlProtocol(poster.shareInfo.poster.image),
-        x: width * 0.03,
-        y: width * 0.21,
+    },
+    {
+      type: 'image',
+      src: formatImageUrlProtocol(poster.shareInfo.poster.image),
+      css: {
+        position: 'fixed',
+        left: width * 0.03,
+        top: width * 0.21,
         width: width * 0.94,
         height: width * 0.94,
-        r: 10,
       },
-      {
-        name: 'goodsTitle',
-        type: 'text',
-        val: poster.shareInfo.poster.title,
-        x: width * 0.04,
-        y: width * 1.18,
-        maxWidth: width * 0.91,
-        line: 2,
+    },
+    {
+      type: 'text',
+      text: poster.shareInfo.poster.title,
+      css: {
+        position: 'fixed',
+        left: width * 0.04,
+        top: width * 1.18,
+        color: '#333',
+        fontSize: 14,
         lineHeight: 5,
-        paintbrushProps: {
-          fillStyle: '#333',
-          font: {
-            fontSize: 14,
-          },
-        },
+        maxWidth: width * 0.91,
       },
-      {
-        name: 'goodsPrice',
-        type: 'text',
-        val: '¥' + poster.shareInfo.poster.price,
-        x: width * 0.04,
-        y: width * 1.3,
-        paintbrushProps: {
-          fillStyle: '#ff0000',
-          font: {
-            fontSize: 20,
-            fontFamily: 'OPPOSANS',
-          },
-        },
+    },
+    {
+      type: 'text',
+      text: '¥' + poster.shareInfo.poster.price,
+      css: {
+        position: 'fixed',
+        left: width * 0.04,
+        top: width * 1.3,
+        fontSize: 20,
+        fontFamily: 'OPPOSANS',
+        color: '#333',
       },
-      {
-        name: 'goodsOriginalPrice',
-        type: 'text',
-        val:
-          poster.shareInfo.poster.original_price > 0
-            ? '¥' + poster.shareInfo.poster.original_price
-            : '',
-        x: width * 0.3,
-        y: width * 1.32,
-        paintbrushProps: {
-          fillStyle: '#999',
-          font: {
-            fontSize: 10,
-            fontFamily: 'OPPOSANS',
-          },
-        },
-        textDecoration: {
-          line: 'line-through',
-          style: 'solide',
-        },
+    },
+    {
+      type: 'text',
+      text:
+        poster.shareInfo.poster.original_price > 0
+          ? '¥' + poster.shareInfo.poster.original_price
+          : '',
+      css: {
+        position: 'fixed',
+        left: width * 0.3,
+        top: width * 1.32,
+        color: '#999',
+        fontSize: 10,
+        fontFamily: 'OPPOSANS',
+        textDecoration: 'line-through',
       },
-      // #ifndef MP-WEIXIN
-      {
-        name: 'qrcode',
-        type: 'qrcode',
-        val: poster.shareInfo.link,
-        x: width * 0.75,
-        y: width * 1.3,
-        size: width * 0.2,
+    },
+    // #ifndef MP-WEIXIN
+    {
+      type: 'qrcode',
+      text: poster.shareInfo.link,
+      css: {
+        position: 'fixed',
+        left: width * 0.75,
+        top: width * 1.3,
+        width: width * 0.2,
+        height: width * 0.2,
       },
-      // #endif
-      // #ifdef MP-WEIXIN
-      {
-        name: 'wxacode',
-        type: 'image',
-        val: wxa_qrcode,
-        x: width * 0.75,
-        y: width * 1.3,
+    },
+    // #endif
+    // #ifdef MP-WEIXIN
+    {
+      type: 'image',
+      src: wxa_qrcode,
+      css: {
+        position: 'fixed',
+        left: width * 0.75,
+        top: width * 1.3,
         width: width * 0.2,
         height: width * 0.2,
       },
-      // #endif
-    ],
-  };
+    },
+    // #endif
+  ];
 };
 
 export default goods;

+ 223 - 0
uni_modules/lime-painter/changelog.md

@@ -0,0 +1,223 @@
+## 1.9.6.5(2024-04-14)
+- fix: 修复`nvue`无法生图的问题
+## 1.9.6.4(2024-03-10)
+- fix: 修复代理ctx导致H5不能使用ctx.save
+## 1.9.6.3(2024-03-08)
+- fix: 修复支付宝真机无法使用的问题
+## 1.9.6.2(2024-02-22)
+- fix: 修复使用render函数报错的问题
+## 1.9.6.1(2023-12-22)
+- fix: 修复字节小程序非2d字体偏移
+- fix: 修复`canvasToTempFilePathSync`会触发两次的问题
+- fix: 修复`parser`图片没有宽度的问题
+## 1.9.6(2023-12-06)
+- fix: 修复背景图受padding影响
+- fix: 修复因字节报错改了代理实现导致微信报错
+- 1.9.5.8(2023-11-16)
+- fix: 修复margin问题
+- fix: 修复borderWidth问题
+- fix: 修复textBox问题
+- fix: 修复字节开发工具报`could not be cloned.`问题
+## 1.9.5.7(2023-07-27)
+- fix: 去掉多余的方法
+- chore: 更新文档,增加自定义字体说明
+## 1.9.5.6(2023-07-21)
+- feat: 有限的支持富文本
+- feat: H5和APP 增加 `hidpi` prop,主要用于大尺寸无法生成图片时用
+- fix: 修复 钉钉小程序 缺少 `measureText` 方法
+- chore: 由于微信小程序 pc 端的 canvas 2d 时不时抽风,故不使用canvas 2d
+## 1.9.5.5(2023-06-27)
+- fix: 修复把`emoji`表情字符拆分成多个字符的情况
+## 1.9.5.4(2023-06-05)
+- fix: 修复因`canvasToTempFilePathSync`监听导致重复调用
+## 1.9.5.3(2023-05-23)
+- fix: 因isPc错写成了isPC导致小程序PC不能生成图片
+## 1.9.5.2(2023-05-22)
+- feat: 删除多余文件
+## 1.9.5.1(2023-05-22)
+- fix: 修复 文字行数与`line-clamp`相同但不满一行时也加了省略号的问题
+## 1.9.5(2023-05-14)
+- feat: 增加 `text-indent` 和 `calc` 方法
+- feat: 优化 布局时间
+## 1.9.4.4(2023-04-15)
+- fix: 修复无法匹配负值
+- fix: 修复 Nvue IOS getImageInfo `useCORS` 为 undefined
+## 1.9.4.3(2023-04-01)
+- feat: 增加支持文字描边 `text-stroke: '5rpx #fff'`
+## 1.9.4.2(2023-03-30)
+- fix: 修复 支付宝小程序 isPC 在手机也为true的问题
+- feat: 由 微信开发工具 3060 版 无法获取图片尺寸,现 微信开发工具 3220 版 修复该问题,故还原上一版的获取图片方式。
+## 1.9.4.1(2023-03-28)
+- fix: 修复固定高度不正确问题
+## 1.9.4(2023-03-17)
+- fix: nvue ios getImageInfo缺少this报错
+- fix: pathType 非2d无效问题
+- fix: 修复 小米9se 可能会存在多次init 导致画面多次放大
+- fix: 修复 border 分开写 width style无效问题
+- fix: 修复 支付宝小程序IOS 再次进入不渲染的问题
+- fix: 修复 支付宝小程序安卓Zindex排序错乱问题
+- fix: 修复 微信开发工具 3060 版 无法获取图片的问题
+- feat: 把 for in 改为 forEach
+- feat: 增加 hidden
+- feat: 根节点 box-sizing 默认 `border-box`
+- feat: 增加支持 `vw` `wh`
+- chore: pathType 取消 默认值,因为字节开发工具不能显示
+- chore: 支付宝小程序开发工具不支持 生成图片 请以真机调试为准
+- bug: 企业微信 2.20.3无法使用
+## 1.9.3.5(2022-06-29)
+- feat: justifyContent 增加 `space-around`、`space-between`
+- feat: canvas 2d 也使用`getImageInfo`
+- fix: 修复 `text`的 `text-decoration`错位
+## 1.9.3.4(2022-06-20)
+- fix: 修复 因创建节点速度问题导致顺序出错。 
+- fix: 修复 微信小程序 PC 无法显示本地图片 
+- fix: 修复 flex-box 对齐问题 
+- feat: 增加 `text-shadow`
+- feat: 重写 `text` 对齐方式
+- chore: 更新文档
+## 1.9.3.3(2022-06-17)
+- fix: 修复 支付宝小程序 canvas 2d 存在ctx.draw问题导致报错
+- fix: 修复 支付宝小程序 toDataURL 存在权限问题改用 `toTempFilePath`
+- fix: 修复 支付宝小程序 image size 问题导致 `objectFit` 无效
+## 1.9.3.2(2022-06-14)
+- fix: 修复 image 设置背景色不生效问题
+- fix: 修复 nvue 环境判断缺少参数问题
+## 1.9.3.1(2022-06-14)
+- fix: 修复 bottom 定位不对问题
+- fix: 修复 因小数导致计算出错换行问题
+- feat: 增加 `useCORS` h5端图片跨域 在设置请求头无效果后试一下设置这个值
+- chore: 更新文档
+## 1.9.3(2022-06-13)
+- feat: 增加 `zIndex`
+- feat: 增加 `flex-box` 该功能处于原始阶段,非常简陋。
+- tips: QQ小程序 vue3 不支持, 为 uni 官方BUG
+## 1.9.2.9(2022-06-10)
+- fix: 修复`text-align`及`margin`居中问题
+## 1.9.2.8(2022-06-10)
+- fix: 修复 Nvue `canvasToTempFilePathSync` 不生效问题
+## 1.9.2.7(2022-06-10)
+- fix: 修复 margin及padding的bug
+- fix: 修复 Nvue `isCanvasToTempFilePath` 不生效问题
+## 1.9.2.6(2022-06-09)
+- fix: 修复 Nvue 不显示
+- feat: 增加支持字体渐变
+```html
+<l-painter-text 
+	text="水调歌头\n明月几时有?把酒问青天。不知天上宫阙,今夕是何年。我欲乘风归去,又恐琼楼玉宇,高处不胜寒。起舞弄清影,何似在人间。"
+	css="background: linear-gradient(,#ff971b 0%, #1989fa 100%); background-clip: text" />
+```
+## 1.9.2.5(2022-06-09)
+- chore: 更变获取父级宽度的设定
+- chore: `pathType` 在canvas 2d 默认为 `url`
+## 1.9.2.4(2022-06-08)
+- fix: 修复 `pathType` 不生效问题
+## 1.9.2.3(2022-06-08)
+- fix: 修复 `canvasToTempFilePath` 漏写 `success` 参数
+## 1.9.2.2(2022-06-07)
+- chore: 更新文档
+## 1.9.2.1(2022-06-07)
+- fix: 修复 vue3 赋值给this再传入导致image无法绘制
+- fix: 修复 `canvasToTempFilePathSync` 时机问题
+- feat: canvas 2d 更改图片生成方式 `toDataURL` 
+## 1.9.2(2022-05-30)
+- fix: 修复 `canvasToTempFilePathSync` 在 vue3 下只生成一次
+## 1.9.1.7(2022-05-28)
+- fix: 修复 `qrcode`显示不全问题
+## 1.9.1.6(2022-05-28)
+- fix: 修复 `canvasToTempFilePathSync` 会重复多次问题
+- fix: 修复 `view` css `backgroundImage` 图片下载失败导致 子节点不渲染
+## 1.9.1.5(2022-05-27)
+- fix: 修正支付宝小程序 canvas 2d版本号 2.7.15
+## 1.9.1.4(2022-05-22)
+- fix: 修复字节小程序无法使用xml方式
+- fix: 修复字节小程序无法使用base64(非2D情况下工具上无法显示)
+- fix: 修复支付宝小程序 `canvasToTempFilePath` 报错
+## 1.9.1.3(2022-04-29)
+- fix: 修复vue3打包后uni对象为空后的报错
+## 1.9.1.2(2022-04-25)
+- fix: 删除多余文件
+## 1.9.1.1(2022-04-25)
+- fix: 修复图片不显示问题
+## 1.9.1(2022-04-12)
+- fix: 因四舍五入导致有些机型错位
+- fix: 修复无views报错 
+- chore: nvue下因ios无法读取插件内static文件,改由下载方式
+## 1.9.0(2022-03-20)
+- fix: 因无法固定尺寸导致生成图片不全
+- fix: 特定情况下text判断无效
+- chore: 本地化APP Nvue webview
+## 1.8.9(2022-02-20)
+- fix: 修复 小程序下载最多10次并发的问题
+- fix: 修复 APP端无法获取本地图片
+- fix: 修复 APP Nvue端不执行问题
+- chore: 增加图片缓存机制
+## 1.8.8.8(2022-01-27)
+- fix: 修复 主动调用尺寸问题
+## 1.8.8.6(2022-01-26)
+- fix: 修复 nvue 下无宽度时获取父级宽度 
+- fix: 修复 ios app 无法渲染问题
+## 1.8.8(2022-01-23)
+- fix: 修复 主动调用时无节点问题
+- fix: 修复 `box-shadow` 颜色问题
+- fix: 修复 `transform:rotate` 角度位置问题
+- feat: 增加 `overflow:hidden`
+## 1.8.7(2022-01-07)
+- fix: 修复 image 方向为 `right` 时原始宽高问题
+- feat: 支持 view 设置背景图 `background-image: url(xxx)`
+- chore: 去掉可选链
+## 1.8.6(2021-11-28)
+- feat: 支持`view`对`inline-block`的子集使用`text-align`
+## 1.8.5.5(2021-08-17)
+- chore: 更新文档,删除 replace
+- fix: 修复 text 值为 number时报错
+## 1.8.5.4(2021-08-16)
+- fix: 字节小程序兼容
+## 1.8.5.3(2021-08-15)
+- fix: 修复线性渐变与css现实效果不一致的问题
+- chore: 更新文档
+## 1.8.5.2(2021-08-13)
+- chore: 增加`background-image`、`background-repeat` 能力,主要用于背景纹理的绘制,并不是代替`image`。例如:大面积的重复平铺的水印
+- 注意:这个功能H5暂时无法使用,因为[官方的API有BUG](https://ask.dcloud.net.cn/question/128793),待官方修复!!!
+## 1.8.5.1(2021-08-10)
+- fix: 修复因`margin`报错问题
+## 1.8.5(2021-08-09)
+- chore: 增加margin支持`auto`,以达到居中效果
+## 1.8.4(2021-08-06)
+- chore: 增加判断缓存文件条件
+- fix: 修复css 多余空格报错问题
+## 1.8.3(2021-08-04)
+- tips: 1.6.x 以下的版本升级到1.8.x后要为每个元素都加上定位:position: 'absolute'
+- fix: 修复只有一个view子元素时不计算高度的问题
+## 1.8.2(2021-08-03)
+- fix: 修复 path-type 为 `url` 无效问题
+- fix: 修复 qrcode `text` 为空时报错问题
+- fix: 修复 image `src` 动态设置时不生效问题
+- feat: 增加 css 属性 `min-width` `max-width`
+## 1.8.1(2021-08-02)
+- fix: 修复无法加载本地图片
+## 1.8.0(2021-08-02)
+- chore 文档更新
+- 使用旧版的同学不要升级!
+## 1.8.0-beta(2021-07-30)
+- ## 全新布局方式 不兼容旧版!
+- chore: 布局方式变更
+- tips: 微信canvas 2d 不支持真机调试
+## 1.6.6(2021-07-09)
+- chore: 统一命名规范,无须主动引入组件
+## 1.6.5(2021-06-08)
+- chore: 去掉console
+## 1.6.4(2021-06-07)
+- fix: 修复 数字 为纯字符串时不转换的BUG
+## 1.6.3(2021-06-06)
+- fix: 修复 PC 端放大的BUG
+## 1.6.2(2021-05-31)
+- fix: 修复 报`adaptor is not a function`错误
+- fix: 修复 text 多行高度
+- fix: 优化 默认文字的基准线
+- feat: `@progress`事件,监听绘制进度
+## 1.6.1(2021-02-28)
+- 删除多余节点
+## 1.6.0(2021-02-26)
+- 调整为uni_modules目录规范
+- 修复:transform的rotate不能为负数问题
+- 新增:`pathType` 指定生成图片返回的路径类型,可选值有 `base64`、`url`

+ 150 - 0
uni_modules/lime-painter/components/common/relation.js

@@ -0,0 +1,150 @@
+const styles = (v ='') =>  v.split(';').filter(v => v && !/^[\n\s]+$/.test(v)).map(v => {
+						const key = v.slice(0, v.indexOf(':'))
+						const value = v.slice(v.indexOf(':')+1)
+						return {
+							[key
+								.replace(/-([a-z])/g, function() { return arguments[1].toUpperCase()})
+								.replace(/\s+/g, '')
+							]: value.replace(/^\s+/, '').replace(/\s+$/, '') || ''
+						}
+					})
+export function parent(parent) {
+	return {
+		provide() {
+			return {
+				[parent]: this
+			}
+		},
+		data() {
+			return {
+				el: {
+					id: null,
+					css: {},
+					views: []
+				},
+			}
+		},
+		watch: {
+			css: { 
+				handler(v) {
+					if(this.canvasId) {
+						this.el.css = (typeof v == 'object' ? v : v && Object.assign(...styles(v))) || {}
+						this.canvasWidth = this.el.css && this.el.css.width || this.canvasWidth
+						this.canvasHeight = this.el.css && this.el.css.height || this.canvasHeight
+					}
+				},
+				immediate: true
+			}
+		}
+	}
+}
+export function children(parent, options = {}) {
+	const indexKey = options.indexKey || 'index'
+	return {
+		inject: {
+			[parent]: {
+				default: null
+			}
+		},
+		watch: {
+			el: {
+				handler(v, o) {
+					if(JSON.stringify(v) != JSON.stringify(o))
+						this.bindRelation()
+				},
+				deep: true,
+				immediate: true
+			},
+			src: {
+				handler(v, o) {
+					if(v != o)
+						this.bindRelation()
+				},
+				immediate: true
+			},
+			text: {
+				handler(v, o) {
+					if(v != o) this.bindRelation()
+				},
+				immediate: true
+			},
+			css: {
+				handler(v, o) {
+					if(v != o)
+						this.el.css = (typeof v == 'object' ? v : v && Object.assign(...styles(v))) || {}
+				},
+				immediate: true
+			},
+			replace: {
+				handler(v, o) {
+					if(JSON.stringify(v) != JSON.stringify(o))
+						this.bindRelation()
+				},
+				deep: true,
+				immediate: true
+			}
+		},
+		created() {
+			if(!this._uid) {
+				this._uid = this._.uid
+			}
+			Object.defineProperty(this, 'parent', {
+				get: () => this[parent] || [],
+			})
+			Object.defineProperty(this, 'index', {
+				get: () =>  {
+					this.bindRelation();
+					const {parent: {el: {views=[]}={}}={}} = this
+					return views.indexOf(this.el)
+				},
+			});
+			this.el.type = this.type
+			if(this.uid) {
+				this.el.uid = this.uid
+			}
+			this.bindRelation()
+		},
+		// #ifdef VUE3
+		beforeUnmount() {
+			this.removeEl()
+		},
+		// #endif
+		// #ifdef VUE2
+		beforeDestroy() {
+			this.removeEl()
+		},
+		// #endif
+		methods: {
+			removeEl() {
+				if (this.parent) {
+					this.parent.el.views = this.parent.el.views.filter(
+						(item) => item._uid !== this._uid
+					);
+				}
+			},
+			bindRelation() {
+				if(!this.el._uid) {
+					this.el._uid = this._uid 
+				}
+				if(['text','qrcode'].includes(this.type)) {
+					this.el.text = this.$slots && this.$slots.default && this.$slots.default[0].text || `${this.text || ''}`.replace(/\\n/g, '\n')
+				}
+				if(this.type == 'image') {
+					this.el.src = this.src
+				}
+				if (!this.parent) {
+					return;
+				}
+				let views = this.parent.el.views || [];
+				if(views.indexOf(this.el) !== -1) {
+					this.parent.el.views = views.map(v => v._uid == this._uid ? this.el : v)
+				} else {
+					this.parent.el.views = [...views, this.el];
+				}
+			}
+		},
+		mounted() {
+			// this.bindRelation()
+		},
+	}
+}

+ 28 - 0
uni_modules/lime-painter/components/l-painter-image/l-painter-image.vue

@@ -0,0 +1,28 @@
+<template>
+	
+</template>
+
+<script>
+	import {parent, children} from '../common/relation';
+	export default {
+		name: 'lime-painter-image',
+		mixins:[children('painter')],
+		props: {
+			id: String,
+			css: [String, Object],
+			src: String
+		},
+		data() {
+			return {
+				type: 'image',
+				el: {
+					css: {},
+					src: null
+				},
+			}
+		}
+	}
+</script>
+
+<style>
+</style>

+ 27 - 0
uni_modules/lime-painter/components/l-painter-qrcode/l-painter-qrcode.vue

@@ -0,0 +1,27 @@
+<template>
+</template>
+
+<script>
+	import {parent, children} from '../common/relation';
+	export default {
+		name: 'lime-painter-qrcode',
+		mixins:[children('painter')],
+		props: {
+			id: String,
+			css: [String, Object],
+			text: String
+		},
+		data() {
+			return {
+				type: 'qrcode',
+				el: {
+					css: {},
+					text: null
+				},
+			}
+		}
+	}
+</script>
+
+<style>
+</style>

+ 33 - 0
uni_modules/lime-painter/components/l-painter-text/l-painter-text.vue

@@ -0,0 +1,33 @@
+<template>
+	<text style="opacity: 0;height: 0;"><slot/></text>
+</template>
+
+<script>
+	import {parent, children} from '../common/relation';
+	export default {
+		name: 'lime-painter-text',
+		mixins:[children('painter')],
+		props: {
+			type: {
+				type: String,
+				default: 'text'
+			},
+			uid: String,
+			css: [String, Object],
+			text: [String, Number],
+			replace: Object,
+		},
+		data() {
+			return {
+				// type: 'text',
+				el: {
+					css: {},
+					text: null
+				},
+			}
+		}
+	}
+</script>
+
+<style>
+</style>

+ 34 - 0
uni_modules/lime-painter/components/l-painter-view/l-painter-view.vue

@@ -0,0 +1,34 @@
+<template>
+	<view><slot/></view>
+</template>
+
+<script>
+	import {parent, children} from '../common/relation';
+	export default {
+		name: 'lime-painter-view',
+		mixins:[children('painter'), parent('painter')],
+		props: {
+			id: String,
+			type: {
+				type: String,
+				default: 'view'
+			},
+			css: [String, Object],
+		},
+		data() {
+			return {
+				// type: 'view',
+				el: {
+					css: {},
+					views:[]
+				},
+			}
+		},
+		mounted() {
+			
+		}
+	}
+</script>
+
+<style>
+</style>

+ 461 - 0
uni_modules/lime-painter/components/l-painter/l-painter.vue

@@ -0,0 +1,461 @@
+<template>
+	<view class="lime-painter" ref="limepainter">
+		<view v-if="canvasId && size" :style="styles">
+			<!-- #ifndef APP-NVUE -->
+			<canvas class="lime-painter__canvas" v-if="use2dCanvas" :id="canvasId" type="2d" :style="size"></canvas>
+			<canvas class="lime-painter__canvas" v-else :id="canvasId" :canvas-id="canvasId" :style="size"
+				:width="boardWidth * dpr" :height="boardHeight * dpr" :hidpi="hidpi"></canvas>
+
+			<!-- #endif -->
+			<!-- #ifdef APP-NVUE -->
+			<web-view :style="size" ref="webview"
+				src="/uni_modules/lime-painter/hybrid/html/index.html"
+				class="lime-painter__canvas" @pagefinish="onPageFinish" @error="onError" @onPostMessage="onMessage">
+			</web-view>
+			<!-- #endif -->
+		</view>
+		<slot />
+	</view>
+</template>
+
+<script>
+	import { parent } from '../common/relation'
+	import props from './props'
+	import {toPx, base64ToPath, pathToBase64, isBase64, sleep, getImageInfo }from './utils';
+	//  #ifndef APP-NVUE
+	import { canIUseCanvas2d, isPC} from './utils';
+	import Painter from './painter';
+	// import Painter from '@painter'
+	const nvue = {}
+	//  #endif
+	//  #ifdef APP-NVUE
+	import nvue from './nvue'
+	//  #endif
+	export default {
+		name: 'lime-painter',
+		mixins: [props, parent('painter'), nvue],
+		data() {
+			return {
+				use2dCanvas: false,
+				canvasHeight: 150,
+				canvasWidth: null,
+				parentWidth: 0,
+				inited: false,
+				progress: 0,
+				firstRender: 0,
+				done: false,
+				tasks: []
+			};
+		},
+		computed: {
+			styles() {
+				return `${this.size}${this.customStyle||''};` + (this.hidden && 'position: fixed; left: 1500rpx;')
+			},
+			canvasId() {
+				return `l-painter${this._ && this._.uid || this._uid}`
+			},
+			size() {
+				if (this.boardWidth && this.boardHeight) {
+					return `width:${this.boardWidth}px; height: ${this.boardHeight}px;`;
+				}
+			},
+			dpr() {
+				return this.pixelRatio || uni.getSystemInfoSync().pixelRatio;
+			},
+			boardWidth() {
+				const {width = 0} = (this.elements && this.elements.css) || this.elements || this
+				const w = toPx(width||this.width)
+				return w || Math.max(w, toPx(this.canvasWidth));
+			},
+			boardHeight() {
+				const {height = 0} = (this.elements && this.elements.css) || this.elements || this
+				const h = toPx(height||this.height)
+				return h || Math.max(h, toPx(this.canvasHeight));
+			},
+			hasBoard() {
+				return this.board && Object.keys(this.board).length
+			},
+			elements() {
+				return this.hasBoard ? this.board : JSON.parse(JSON.stringify(this.el))
+			}
+		},
+		created() {
+			this.use2dCanvas = this.type === '2d' && canIUseCanvas2d() && !isPC
+		},
+		async mounted() {
+			await sleep(30)
+			await this.getParentWeith()
+			this.$nextTick(() => {
+				setTimeout(() => {
+					this.$watch('elements', this.watchRender, {
+						deep: true,
+						immediate: true
+					});
+				}, 30)
+			})
+		},
+		// #ifdef VUE3
+		unmounted() {
+			this.done = false
+			this.inited = false
+			this.firstRender = 0
+			this.progress = 0
+			this.painter = null
+			clearTimeout(this.rendertimer)
+		},
+		// #endif
+		// #ifdef VUE2
+		destroyed() {
+			this.done = false
+			this.inited = false
+			this.firstRender = 0
+			this.progress = 0
+			this.painter = null
+			clearTimeout(this.rendertimer)
+		},
+		// #endif
+		methods: {
+			async watchRender(val, old) {
+				if (!val || !val.views || (!this.firstRender ? !val.views.length : !this.firstRender) || !Object.keys(val).length || JSON.stringify(val) == JSON.stringify(old)) return;
+				this.firstRender = 1
+				this.progress = 0
+				this.done = false
+				clearTimeout(this.rendertimer)
+				this.rendertimer = setTimeout(() => {
+					this.render(val);
+				}, this.beforeDelay)
+			},
+			async setFilePath(path, param) {
+				let filePath = path
+				const {pathType = this.pathType} =  param || this
+				if (pathType == 'base64' && !isBase64(path)) {
+					filePath = await pathToBase64(path)
+				} else if (pathType == 'url' && isBase64(path)) {
+					filePath = await base64ToPath(path)
+				}
+				if (param && param.isEmit) {
+					this.$emit('success', filePath);
+				}
+				return filePath
+			},
+			async getSize(args) {
+				const {width} = args.css || args
+				const {height} = args.css || args
+				if (!this.size) {
+					if (width || height) {
+						this.canvasWidth = width || this.canvasWidth
+						this.canvasHeight = height || this.canvasHeight
+						await sleep(30);
+					} else {
+						await this.getParentWeith()
+					}
+				}
+			},
+			canvasToTempFilePathSync(args) {
+				// this.stopWatch && this.stopWatch()
+				// this.stopWatch = this.$watch('done', (v) => {
+				// 	if (v) {
+				// 		this.canvasToTempFilePath(args)
+				// 		this.stopWatch && this.stopWatch()
+				// 	}
+				// }, {
+				// 	immediate: true
+				// })
+				this.tasks.push(args)
+				if(this.done){
+					this.runTask()
+				}
+			},
+			runTask(){
+				while(this.tasks.length){
+					const task = this.tasks.shift()	
+					 this.canvasToTempFilePath(task)
+				}
+			},
+			// #ifndef APP-NVUE
+			getParentWeith() {
+				return new Promise(resolve => {
+					uni.createSelectorQuery()
+						.in(this)
+						.select(`.lime-painter`)
+						.boundingClientRect()
+						.exec(res => {
+							const {width, height} = res[0]||{}
+							this.parentWidth = Math.ceil(width||0)
+							this.canvasWidth = this.parentWidth || 300
+							this.canvasHeight = height || this.canvasHeight||150
+							resolve(res[0])
+						})
+				})
+			},
+			async render(args = {}) {
+				if(!Object.keys(args).length) {
+					return console.error('空对象')
+				}
+				this.progress = 0
+				this.done = false
+				// #ifdef APP-NVUE
+				this.tempFilePath.length = 0
+				// #endif
+				await this.getSize(args)
+				const ctx = await this.getContext();
+				
+				let {
+					use2dCanvas,
+					boardWidth,
+					boardHeight,
+					canvas,
+					afterDelay
+				} = this;
+				if (use2dCanvas && !canvas) {
+					return Promise.reject(new Error('canvas 没创建'));
+				}
+				this.boundary = {
+					top: 0,
+					left: 0,
+					width: boardWidth,
+					height: boardHeight
+				};
+				this.painter = null
+				if (!this.painter) {
+					const {width} = args.css || args
+					const {height} = args.css || args
+					if(!width && this.parentWidth) {
+						Object.assign(args, {width: this.parentWidth})
+					}
+					const param = {
+						context: ctx,
+						canvas,
+						width: boardWidth,
+						height: boardHeight,
+						pixelRatio: this.dpr,
+						useCORS: this.useCORS,
+						createImage: getImageInfo.bind(this),
+						performance: this.performance,
+						listen: {
+							onProgress: (v) => {
+								this.progress = v
+								this.$emit('progress', v)
+							},
+							onEffectFail: (err) => {
+								this.$emit('faill', err)
+							}
+						}
+					}
+					this.painter = new Painter(param)
+				} 
+				try{
+					// vue3 赋值给data会引起图片无法绘制
+					const { width, height } = await this.painter.source(JSON.parse(JSON.stringify(args)))
+					this.boundary.height = this.canvasHeight = height
+					this.boundary.width = this.canvasWidth = width
+					await sleep(this.sleep);
+					await this.painter.render()
+					await new Promise(resolve => this.$nextTick(resolve));
+					if (!use2dCanvas) {
+						await this.canvasDraw();
+					}
+					if (afterDelay && use2dCanvas) {
+						await sleep(afterDelay);
+					}
+					this.$emit('done');
+					this.done = true
+					if (this.isCanvasToTempFilePath) {
+						this.canvasToTempFilePath()
+							.then(res => {
+								this.$emit('success', res.tempFilePath)
+							})
+							.catch(err => {
+								this.$emit('fail', new Error(JSON.stringify(err)));
+							});
+					}
+					this.runTask()
+					return Promise.resolve({
+						ctx,
+						draw: this.painter,
+						node: this.node
+					});
+				}catch(e){
+					//TODO handle the exception
+				}
+				
+			},
+			canvasDraw(flag = false) {
+				return new Promise((resolve, reject) => this.ctx.draw(flag, () => setTimeout(() => resolve(), this
+					.afterDelay)));
+			},
+			async getContext() {
+				if (!this.canvasWidth) {
+					this.$emit('fail', 'painter no size')
+					console.error('[lime-painter]: 给画板或父级设置尺寸')
+					return Promise.reject();
+				}
+				if (this.ctx && this.inited) {
+					return Promise.resolve(this.ctx);
+				}
+				const { type, use2dCanvas, dpr, boardWidth, boardHeight } = this;
+				const _getContext = () => {
+					return new Promise(resolve => {
+						uni.createSelectorQuery()
+							.in(this)
+							.select(`#${this.canvasId}`)
+							.boundingClientRect()
+							.exec(res => {
+								if (res) {
+									const ctx = uni.createCanvasContext(this.canvasId, this);
+									if (!this.inited) {
+										this.inited = true;
+										this.use2dCanvas = false;
+										this.canvas = res;
+									}
+									
+									// 钉钉小程序框架不支持 measureText 方法,用此方法 mock
+									if (!ctx.measureText) {
+										function strLen(str) {
+											let len = 0;
+											for (let i = 0; i < str.length; i++) {
+												if (str.charCodeAt(i) > 0 && str.charCodeAt(i) < 128) {
+													len++;
+												} else {
+													len += 2;
+												}
+											}
+											return len;
+										}
+										ctx.measureText = text => {
+											let fontSize = ctx.state && ctx.state.fontSize || 12;
+											const font = ctx.__font
+											if (font && fontSize == 12) {
+												fontSize = parseInt(font.split(' ')[3], 10);
+											}
+											fontSize /= 2;
+											return {
+												width: strLen(text) * fontSize
+											};
+										}
+									}
+									
+									// #ifdef MP-ALIPAY
+									ctx.scale(dpr, dpr);
+									// #endif
+									this.ctx = ctx
+									resolve(this.ctx);
+								} else {
+									console.error('[lime-painter] no node')
+								}
+							});
+					});
+				};
+				if (!use2dCanvas) {
+					return _getContext();
+				}
+				return new Promise(resolve => {
+					uni.createSelectorQuery()
+						.in(this)
+						.select(`#${this.canvasId}`)
+						.node()
+						.exec(res => {
+							let {node: canvas} = res && res[0]||{};
+							if(canvas) {
+								const ctx = canvas.getContext(type);
+								if (!this.inited) {
+									this.inited = true;
+									this.use2dCanvas = true;
+									this.canvas = canvas;
+								}
+								this.ctx = ctx
+								resolve(this.ctx);
+							} else {
+								console.error('[lime-painter]: no size')
+							}
+						});
+				});
+			},
+			canvasToTempFilePath(args = {}) {
+				return new Promise(async (resolve, reject) => {
+					const { use2dCanvas, canvasId, dpr, fileType, quality } = this;
+					const success = async (res) => {
+						try {
+							const tempFilePath = await this.setFilePath(res.tempFilePath || res, args)
+							const result = Object.assign(res, {tempFilePath})
+							args.success && args.success(result)
+							resolve(result)
+						} catch (e) {
+							this.$emit('fail', e)
+						}
+					}
+					
+					let { top: y = 0, left: x = 0, width, height } = this.boundary || this;
+					// let destWidth = width * dpr;
+					// let destHeight = height * dpr;
+					// #ifdef MP-ALIPAY
+					// width = destWidth;
+					// height = destHeight;
+					// #endif
+					
+					const copyArgs = Object.assign({
+						// x,
+						// y,
+						// width,
+						// height,
+						// destWidth,
+						// destHeight,
+						canvasId,
+						id: canvasId,
+						fileType,
+						quality,
+					}, args, {success});
+					// if(this.isPC || use2dCanvas) {
+					// 	copyArgs.canvas = this.canvas
+					// }
+					if (use2dCanvas) {
+						copyArgs.canvas = this.canvas
+						try{
+							// #ifndef MP-ALIPAY
+							const oFilePath = this.canvas.toDataURL(`image/${args.fileType||fileType}`.replace(/pg/, 'peg'), args.quality||quality)
+							if(/data:,/.test(oFilePath)) {
+								uni.canvasToTempFilePath(copyArgs, this);
+							} else {
+								const tempFilePath = await this.setFilePath(oFilePath, args)
+								args.success && args.success({tempFilePath})
+								resolve({tempFilePath})
+							}
+							// #endif
+							// #ifdef MP-ALIPAY
+							this.canvas.toTempFilePath(copyArgs)
+							// #endif
+						}catch(e){
+							args.fail && args.fail(e)
+							reject(e)
+						}
+					} else {
+						// #ifdef MP-ALIPAY
+						if(this.ctx.toTempFilePath) {
+							// 钉钉
+							const ctx = uni.createCanvasContext(canvasId);
+							ctx.toTempFilePath(copyArgs);
+						} else {
+							my.canvasToTempFilePath(copyArgs);
+						}
+						// #endif
+						// #ifndef MP-ALIPAY
+						uni.canvasToTempFilePath(copyArgs, this);
+						// #endif
+					}
+				})
+			}
+			// #endif
+		}
+	};
+</script>
+<style>
+	.lime-painter,
+	.lime-painter__canvas {
+		// #ifndef APP-NVUE
+		width: 100%;
+		// #endif
+		// #ifdef APP-NVUE
+		flex: 1;
+		// #endif
+	}
+</style>

+ 214 - 0
uni_modules/lime-painter/components/l-painter/nvue.js

@@ -0,0 +1,214 @@
+// #ifdef APP-NVUE
+import {
+	sleep,
+	getImageInfo,
+	isBase64,
+	networkReg
+} from './utils';
+const dom = weex.requireModule('dom')
+import {
+	version
+} from '../../package.json'
+
+export default {
+	data() {
+		return {
+			tempFilePath: [],
+			isInitFile: false,
+			osName: uni.getSystemInfoSync().osName
+		}
+	},
+	methods: {
+		getParentWeith() {
+			return new Promise(resolve => {
+				dom.getComponentRect(this.$refs.limepainter, (res) => {
+					this.parentWidth = Math.ceil(res.size.width)
+					this.canvasWidth = this.canvasWidth || this.parentWidth || 300
+					this.canvasHeight = res.size.height || this.canvasHeight || 150
+					resolve(res.size)
+				})
+			})
+		},
+		onPageFinish() {
+			this.webview = this.$refs.webview
+			this.webview.evalJS(`init(${this.dpr})`)
+		},
+		onMessage(e) {
+			const res = e.detail.data[0] || null;
+			if (res.event) {
+				if (res.event == 'inited') {
+					this.inited = true
+				}
+				if (res.event == 'fail') {
+					this.$emit('fail', res)
+				}
+				if (res.event == 'layoutChange') {
+					const data = typeof res.data == 'string' ? JSON.parse(res.data) : res.data
+					this.canvasWidth = Math.ceil(data.width);
+					this.canvasHeight = Math.ceil(data.height);
+				}
+				if (res.event == 'progressChange') {
+					this.progress = res.data * 1
+				}
+				if (res.event == 'file') {
+					this.tempFilePath.push(res.data)
+					if (this.tempFilePath.length > 7) {
+						this.tempFilePath.shift()
+					}
+					return
+				}
+				if (res.event == 'success') {
+					if (res.data) {
+						this.tempFilePath.push(res.data)
+						if (this.tempFilePath.length > 8) {
+							this.tempFilePath.shift()
+						}
+						if (this.isCanvasToTempFilePath) {
+							this.setFilePath(this.tempFilePath.join(''), {
+								isEmit: true
+							})
+						}
+					} else {
+						this.$emit('fail', 'canvas no data')
+					}
+					return
+				}
+				this.$emit(res.event, JSON.parse(res.data));
+			} else if (res.file) {
+				this.file = res.data;
+			} else {
+				console.info(res[0])
+			}
+		},
+		getWebViewInited() {
+			if (this.inited) return Promise.resolve(this.inited);
+			return new Promise((resolve) => {
+				this.$watch(
+					'inited',
+					async val => {
+						if (val) {
+							resolve(val)
+						}
+					}, {
+						immediate: true
+					}
+				);
+			})
+		},
+		getTempFilePath() {
+			if (this.tempFilePath.length == 8) return Promise.resolve(this.tempFilePath)
+			return new Promise((resolve) => {
+				this.$watch(
+					'tempFilePath',
+					async val => {
+						if (val.length == 8) {
+							resolve(val.join(''))
+						}
+					}, {
+						deep: true
+					}
+				);
+			})
+		},
+		getWebViewDone() {
+			if (this.progress == 1) return Promise.resolve(this.progress);
+			return new Promise((resolve) => {
+				this.$watch(
+					'progress',
+					async val => {
+						if (val == 1) {
+							this.$emit('done')
+							this.done = true
+							this.runTask()
+							resolve(val)
+						}
+					}, {
+						immediate: true
+					}
+				);
+			})
+		},
+		async render(args) {
+			try {
+				await this.getSize(args)
+				const {
+					width
+				} = args.css || args
+				if (!width && this.parentWidth) {
+					Object.assign(args, {
+						width: this.parentWidth
+					})
+				}
+				const newNode = await this.calcImage(args);
+				await this.getWebViewInited()
+				this.webview.evalJS(`source(${JSON.stringify(newNode)})`)
+				await this.getWebViewDone()
+				await sleep(this.afterDelay)
+				if (this.isCanvasToTempFilePath) {
+					const params = {
+						fileType: this.fileType,
+						quality: this.quality
+					}
+					this.webview.evalJS(`save(${JSON.stringify(params)})`)
+				}
+				return Promise.resolve()
+			} catch (e) {
+				this.$emit('fail', e)
+			}
+		},
+		async calcImage(args) {
+			let node = JSON.parse(JSON.stringify(args))
+			const urlReg = /url\((.+)\)/
+			const {
+				backgroundImage
+			} = node.css || {}
+			const isBG = backgroundImage && urlReg.exec(backgroundImage)[1]
+			const url = node.url || node.src || isBG
+			if (['text', 'qrcode'].includes(node.type)) {
+				return node
+			}
+			if ((node.type === "image" || isBG) && url && !isBase64(url) && (this.osName == 'ios' || !networkReg
+					.test(url))) {
+				let {
+					path
+				} = await getImageInfo(url, true)
+				if (isBG) {
+					node.css.backgroundImage = `url(${path})`
+				} else {
+					node.src = path
+				}
+			} else if (node.views && node.views.length) {
+				for (let i = 0; i < node.views.length; i++) {
+					node.views[i] = await this.calcImage(node.views[i])
+				}
+			}
+			return node
+		},
+		async canvasToTempFilePath(args = {}) {
+			if (!this.inited) {
+				return this.$emit('fail', 'no init')
+			}
+			this.tempFilePath = []
+			if (args.fileType == 'jpg') {
+				args.fileType = 'jpeg'
+			}
+
+			this.webview.evalJS(`save(${JSON.stringify(args)})`)
+			try {
+				let tempFilePath = await this.getTempFilePath()
+
+				tempFilePath = await this.setFilePath(tempFilePath, args)
+				args.success({
+					errMsg: "canvasToTempFilePath:ok",
+					tempFilePath
+				})
+			} catch (e) {
+				console.log('e', e)
+				args.fail({
+					error: e
+				})
+			}
+		}
+	}
+}
+// #endif

File diff suppressed because it is too large
+ 0 - 0
uni_modules/lime-painter/components/l-painter/painter.js


+ 56 - 0
uni_modules/lime-painter/components/l-painter/props.js

@@ -0,0 +1,56 @@
+export default {
+	props: {
+		board: Object,
+		pathType: String, // 'base64'、'url'
+		fileType: {
+			type: String,
+			default: 'png'
+		},
+		hidden: Boolean,
+		quality: {
+			type: Number,
+			default: 1
+		},
+		css: [String, Object],
+		// styles: [String, Object],
+		width: [Number, String],
+		height: [Number, String],
+		pixelRatio: Number,
+		customStyle: String,
+		isCanvasToTempFilePath: Boolean,
+		// useCanvasToTempFilePath: Boolean,
+		sleep: {
+			type: Number,
+			default: 1000 / 30
+		},
+		beforeDelay: {
+			type: Number,
+			default: 100
+		},
+		afterDelay: {
+			type: Number,
+			default: 100
+		},
+		performance: Boolean,
+		// #ifdef MP-WEIXIN || MP-TOUTIAO || MP-ALIPAY
+		type: {
+			type: String,
+			default: '2d'
+		},
+		// #endif
+		// #ifdef APP-NVUE
+		hybrid: Boolean,
+		timeout: {
+			type: Number,
+			default: 2000
+		},
+		// #endif
+		// #ifdef H5 || APP-PLUS
+		useCORS: Boolean,
+		hidpi: {
+			type: Boolean,
+			default: true
+		}
+		// #endif
+	}
+}

File diff suppressed because it is too large
+ 0 - 0
uni_modules/lime-painter/components/l-painter/single.js


+ 368 - 0
uni_modules/lime-painter/components/l-painter/utils.js

@@ -0,0 +1,368 @@
+export const networkReg = /^(http|\/\/)/;
+export const isBase64 = (path) => /^data:image\/(\w+);base64/.test(path);
+export function sleep(delay) {
+	return new Promise(resolve => setTimeout(resolve, delay))
+}
+let {platform, SDKVersion} = uni.getSystemInfoSync() 
+export const isPC = /windows|mac/.test(platform)
+// 缓存图片
+let cache = {}
+export function isNumber(value) {
+	return /^-?\d+(\.\d+)?$/.test(value);
+}
+export function toPx(value, baseSize, isDecimal = false) {
+	// 如果是数字
+	if (typeof value === 'number') {
+		return value
+	}
+	// 如果是字符串数字
+	if (isNumber(value)) {
+		return value * 1
+	}
+	// 如果有单位
+	if (typeof value === 'string') {
+		const reg = /^-?([0-9]+)?([.]{1}[0-9]+){0,1}(em|rpx|px|%)$/g
+		const results = reg.exec(value);
+		if (!value || !results) {
+			return 0;
+		}
+		const unit = results[3];
+		value = parseFloat(value);
+		let res = 0;
+		if (unit === 'rpx') {
+			res = uni.upx2px(value);
+		} else if (unit === 'px') {
+			res = value * 1;
+		} else if (unit === '%') {
+			res = value * toPx(baseSize) / 100;
+		} else if (unit === 'em') {
+			res = value * toPx(baseSize || 14);
+		}
+		return isDecimal ? res.toFixed(2) * 1 : Math.round(res);
+	}
+	return 0
+}
+
+// 计算版本
+export function compareVersion(v1, v2) {
+	v1 = v1.split('.')
+	v2 = v2.split('.')
+	const len = Math.max(v1.length, v2.length)
+	while (v1.length < len) {
+		v1.push('0')
+	}
+	while (v2.length < len) {
+		v2.push('0')
+	}
+	for (let i = 0; i < len; i++) {
+		const num1 = parseInt(v1[i], 10)
+		const num2 = parseInt(v2[i], 10)
+
+		if (num1 > num2) {
+			return 1
+		} else if (num1 < num2) {
+			return -1
+		}
+	}
+	return 0
+}
+
+function gte(version) {
+  // #ifdef MP-ALIPAY
+  SDKVersion = my.SDKVersion
+  // #endif
+  return compareVersion(SDKVersion, version) >= 0;
+}
+export function canIUseCanvas2d() {
+	// #ifdef MP-WEIXIN
+	return gte('2.9.2');
+	// #endif
+	// #ifdef MP-ALIPAY
+	return gte('2.7.15');
+	// #endif
+	// #ifdef MP-TOUTIAO
+	return gte('1.78.0');
+	// #endif
+	return false
+}
+
+// #ifdef MP
+export const prefix = () => {
+	// #ifdef MP-TOUTIAO
+	return tt
+	// #endif
+	// #ifdef MP-WEIXIN
+	return wx
+	// #endif
+	// #ifdef MP-BAIDU
+	return swan
+	// #endif
+	// #ifdef MP-ALIPAY
+	return my
+	// #endif
+	// #ifdef MP-QQ
+	return qq
+	// #endif
+	// #ifdef MP-360
+	return qh
+	// #endif
+}
+// #endif
+
+
+
+/**
+ * base64转路径
+ * @param {Object} base64
+ */
+export function base64ToPath(base64) {
+	const [, format] = /^data:image\/(\w+);base64,/.exec(base64) || [];
+
+	return new Promise((resolve, reject) => {
+		// #ifdef MP
+		const fs = uni.getFileSystemManager()
+		//自定义文件名
+		if (!format) {
+			reject(new Error('ERROR_BASE64SRC_PARSE'))
+		}
+		const time = new Date().getTime();
+		let pre = prefix()
+		// #ifdef MP-TOUTIAO
+		const filePath = `${pre.getEnvInfoSync().common.USER_DATA_PATH}/${time}.${format}`
+		// #endif
+		// #ifndef MP-TOUTIAO
+		const filePath = `${pre.env.USER_DATA_PATH}/${time}.${format}`
+		// #endif
+		fs.writeFile({
+			filePath,
+			data: base64.split(',')[1],
+			encoding: 'base64',
+			success() {
+				resolve(filePath)
+			},
+			fail(err) {
+				console.error(err)
+				reject(err)
+			}
+		})
+		// #endif
+
+		// #ifdef H5
+		// mime类型
+		let mimeString = base64.split(',')[0].split(':')[1].split(';')[0];
+		//base64 解码
+		let byteString = atob(base64.split(',')[1]);
+		//创建缓冲数组
+		let arrayBuffer = new ArrayBuffer(byteString.length);
+		//创建视图
+		let intArray = new Uint8Array(arrayBuffer);
+		for (let i = 0; i < byteString.length; i++) {
+			intArray[i] = byteString.charCodeAt(i);
+		}
+		resolve(URL.createObjectURL(new Blob([intArray], {
+			type: mimeString
+		})))
+		// #endif
+
+		// #ifdef APP-PLUS
+		const bitmap = new plus.nativeObj.Bitmap('bitmap' + Date.now())
+		bitmap.loadBase64Data(base64, () => {
+			if (!format) {
+				reject(new Error('ERROR_BASE64SRC_PARSE'))
+			}
+			const time = new Date().getTime();
+			const filePath = `_doc/uniapp_temp/${time}.${format}`
+			bitmap.save(filePath, {},
+				() => {
+					bitmap.clear()
+					resolve(filePath)
+				},
+				(error) => {
+					bitmap.clear()
+					reject(error)
+				})
+		}, (error) => {
+			bitmap.clear()
+			reject(error)
+		})
+		// #endif
+	})
+}
+
+/**
+ * 路径转base64
+ * @param {Object} string
+ */
+export function pathToBase64(path) {
+	if (/^data:/.test(path)) return path
+	return new Promise((resolve, reject) => {
+		// #ifdef H5
+		let image = new Image();
+		image.setAttribute("crossOrigin", 'Anonymous');
+		image.onload = function() {
+			let canvas = document.createElement('canvas');
+			canvas.width = this.naturalWidth;
+			canvas.height = this.naturalHeight;
+			canvas.getContext('2d').drawImage(image, 0, 0);
+			let result = canvas.toDataURL('image/png')
+			resolve(result);
+			canvas.height = canvas.width = 0
+		}
+		image.src = path + '?v=' + Math.random()
+		image.onerror = (error) => {
+			reject(error);
+		};
+		// #endif
+
+		// #ifdef MP
+		if (uni.canIUse('getFileSystemManager')) {
+			uni.getFileSystemManager().readFile({
+				filePath: path,
+				encoding: 'base64',
+				success: (res) => {
+					resolve('data:image/png;base64,' + res.data)
+				},
+				fail: (error) => {
+					console.error({error, path})
+					reject(error)
+				}
+			})
+		}
+		// #endif
+
+		// #ifdef APP-PLUS
+		plus.io.resolveLocalFileSystemURL(getLocalFilePath(path), (entry) => {
+			entry.file((file) => {
+				const fileReader = new plus.io.FileReader()
+				fileReader.onload = (data) => {
+					resolve(data.target.result)
+				}
+				fileReader.onerror = (error) => {
+					reject(error)
+				}
+				fileReader.readAsDataURL(file)
+			}, reject)
+		}, reject)
+		// #endif
+	})
+}
+
+
+
+export function getImageInfo(path, useCORS) {
+	const isCanvas2D = this && this.canvas && this.canvas.createImage
+	return new Promise(async (resolve, reject) => {
+		// let time = +new Date()
+		let src = path.replace(/^@\//,'/')
+		if (cache[path] && cache[path].errMsg) {
+			resolve(cache[path])
+		} else {
+			try {
+				// #ifdef MP || APP-PLUS
+				if (isBase64(path) && (isCanvas2D ? isPC : true)) {
+					src = await base64ToPath(path)
+				}
+				// #endif
+				// #ifdef H5
+				if(useCORS) {
+					src = await pathToBase64(path)
+				}
+				// #endif
+			} catch (error) {
+				reject({
+					...error,
+					src
+				})
+			}
+			// #ifndef APP-NVUE
+			if(isCanvas2D && !isPC) {
+				const img = this.canvas.createImage()
+				img.onload = function() {
+					const image = {
+						path: img,
+						width:  img.width,
+						height:  img.height
+					}
+					cache[path] = image
+					resolve(cache[path])
+				}
+				img.onerror = function(err) {
+					reject({err,path})
+				}
+				img.src = src
+				return
+			}
+			// #endif
+			uni.getImageInfo({
+				src,
+				success: (image) => {
+					const localReg = /^\.|^\/(?=[^\/])/;
+					// #ifdef MP-WEIXIN || MP-BAIDU || MP-QQ || MP-TOUTIAO
+					image.path = localReg.test(src) ?  `/${image.path}` : image.path;
+					// #endif
+					if(isCanvas2D) {
+						const img = this.canvas.createImage()
+						img.onload = function() {
+							image.path = img
+							cache[path] = image
+							resolve(cache[path])
+						}
+						img.onerror = function(err) {
+							reject({err,path})
+						}
+						img.src = src
+						return
+					}
+					// #ifdef APP-PLUS
+					// console.log('getImageInfo', +new Date() - time)
+					// ios 比较严格 可能需要设置跨域
+					if(uni.getSystemInfoSync().osName == 'ios' && useCORS) {
+						pathToBase64(image.path).then(base64 => {
+							image.path = base64
+							cache[path] = image
+							resolve(cache[path])
+						}).catch(err => {
+							console.error({err, path})
+							reject({err,path})
+						})
+						return
+					}
+					// #endif
+					cache[path] = image
+					resolve(cache[path])
+				},
+				fail(err) {
+					console.error({err, path})
+					reject({err,path})
+				}
+			})
+		}
+	})
+}
+
+
+// #ifdef APP-PLUS
+const getLocalFilePath = (path) => {
+	if (path.indexOf('_www') === 0 || path.indexOf('_doc') === 0 || path.indexOf('_documents') === 0 || path
+		.indexOf('_downloads') === 0) {
+		return path
+	}
+	if (path.indexOf('file://') === 0) {
+		return path
+	}
+	if (path.indexOf('/storage/emulated/0/') === 0) {
+		return path
+	}
+	if (path.indexOf('/') === 0) {
+		const localFilePath = plus.io.convertAbsoluteFileSystem(path)
+		if (localFilePath !== path) {
+			return localFilePath
+		} else {
+			path = path.substr(1)
+		}
+	}
+	return '_www/' + path
+}
+// #endif
+
+

File diff suppressed because it is too large
+ 179 - 0
uni_modules/lime-painter/components/lime-painter/lime-painter.vue


+ 119 - 0
uni_modules/lime-painter/hybrid/html/index.html

@@ -0,0 +1,119 @@
+<!DOCTYPE html>
+<html lang="zh">
+
+<head>
+	<meta charset="UTF-8">
+	<meta name="viewport" content="width=device-width, initial-scale=1.0">
+	<meta http-equiv="X-UA-Compatible" content="ie=edge">
+	<title></title>
+	<style type="text/css">
+		html,
+		body,
+		canvas {
+			padding: 0;
+			margin: 0;
+			width: 100%;
+			height: 100%;
+			overflow-y: hidden;
+			background-color: transparent;
+		}
+	</style>
+</head>
+
+<body>
+	<canvas id="lime-painter"></canvas>
+	<script type="text/javascript" src="./uni.webview.1.5.3.js"></script>
+	<script type="text/javascript" src="./painter.js"></script>
+	<script> 
+		var cache = [];
+		var painter = null;
+		var canvas = null;
+		var context = null;
+		var timer = null;
+		var pixelRatio = 1;
+		console.log = function (...args) {
+			postMessage(args);
+		};
+		// function stringify(key, value) {
+		// 	if (typeof value === 'object' && value !== null) {
+		// 		if (cache.indexOf(value) !== -1) {
+		// 			return;
+		// 		}
+		// 		cache.push(value);
+		// 	}
+		// 	return value;
+		// };
+
+		function emit(event, data) {
+			postMessage({
+				event,
+				data: (typeof data !== 'object' && data !== null ? data : JSON.stringify(data)) 
+			});
+			cache = [];
+		};
+		function postMessage(data) {
+			uni.postMessage({
+				data
+			});
+		};
+		
+		function init(dpr) {
+			canvas = document.querySelector('#lime-painter');
+			context = canvas.getContext('2d');
+			pixelRatio = dpr || window.devicePixelRatio;
+			painter = new Painter({
+				id: 'lime-painter',
+				context,
+				canvas,
+				pixelRatio,
+				width: canvas.offsetWidth,
+				height: canvas.offsetHeight,
+				listen: {
+					onProgress(v) {
+						emit('progressChange', v);
+					},
+					onEffectFail(err) {
+						//console.error(err)
+						emit('fail', err);
+					}
+				}
+			});
+			emit('inited', true);
+		};
+		function save(args) {
+			delete args.success;
+			delete args.fail;
+			clearTimeout(timer);
+			timer = setTimeout(() => {
+				const path = painter.save(args);
+				if (typeof path == 'string') {
+					const index = Math.ceil(path.length / 8);
+					for (var i = 0; i < 8; i++) {
+						if (i == 7) {
+							emit('success', path.substr(i * index, index));
+						} else {
+							emit('file', path.substr(i * index, index));
+						}
+					};
+				} else {
+					// console.log('canvas no data')
+					emit('fail', 'canvas no data');
+				};
+			}, 30);
+		};
+		async function source(args) {
+			let size = await painter.source(args);
+			emit('layoutChange', size);
+			if(!canvas.height) {
+				console.log('canvas no size')
+				emit('fail', 'canvas no size');
+			}
+			painter.render().catch(err => {
+				// console.error(err)
+				emit('fail', err);
+			});
+		};
+	</script>
+</body>
+
+</html>

File diff suppressed because it is too large
+ 0 - 0
uni_modules/lime-painter/hybrid/html/painter.js


File diff suppressed because it is too large
+ 0 - 0
uni_modules/lime-painter/hybrid/html/uni.webview.1.5.3.js


+ 93 - 0
uni_modules/lime-painter/package.json

@@ -0,0 +1,93 @@
+{
+  "id": "lime-painter",
+  "displayName": "海报画板",
+  "version": "1.9.6.5",
+  "description": "一款canvas海报组件,更优雅的海报生成方案,有限的支持富文本",
+  "keywords": [
+    "海报",
+    "富文本",
+    "生成海报",
+    "生成二维码",
+    "JSON"
+],
+  "repository": "https://gitee.com/liangei/lime-painter", 
+  "engines": {
+    "HBuilderX": "^3.4.14"
+  },
+"dcloudext": {
+    "sale": {
+      "regular": {
+        "price": "0.00"
+      },
+      "sourcecode": {
+        "price": "0.00"
+      }
+    },
+    "contact": {
+      "qq": "305716444"
+    },
+    "declaration": {
+      "ads": "无",
+      "data": "无",
+      "permissions": "无"
+    },
+    "npmurl": "",
+    "type": "component-vue"
+  },
+  "uni_modules": {
+    "dependencies": [],
+    "encrypt": [],
+    "platforms": {
+      "cloud": {
+        "tcb": "y",
+        "aliyun": "y",
+        "alipay": "n"
+      },
+      "client": {
+        "App": {
+          "app-vue": "y",
+          "app-nvue": "y"
+        },
+        "H5-mobile": {
+          "Safari": "y",
+          "Android Browser": "y",
+          "微信浏览器(Android)": "y",
+          "QQ浏览器(Android)": "y"
+        },
+        "H5-pc": {
+          "Chrome": "y",
+          "IE": "u",
+          "Edge": "u",
+          "Firefox": "u",
+          "Safari": "y"
+        },
+        "小程序": {
+          "微信": "y",
+          "阿里": "y",
+          "百度": "y",
+          "字节跳动": "y",
+        "QQ": "y",
+        "钉钉": "u",
+        "快手": "u",
+        "飞书": "u",
+        "京东": "u"
+        },
+        "快应用": {
+          "华为": "u",
+          "联盟": "u"
+        },
+        "Vue": {
+            "vue2": "y",
+            "vue3": "y"
+        }
+      }
+    }
+  },
+  "name": "lime-painter",
+  "main": "index.js",
+  "scripts": {
+    "test": "echo \"Error: no test specified\" && exit 1"
+  },
+  "author": "",
+  "license": "ISC"
+}

+ 388 - 0
uni_modules/lime-painter/parser.js

@@ -0,0 +1,388 @@
+/*
+ * HTML5 Parser By Sam Blowes
+ *
+ * Designed for HTML5 documents
+ *
+ * Original code by John Resig (ejohn.org)
+ * http://ejohn.org/blog/pure-javascript-html-parser/
+ * Original code by Erik Arvidsson, Mozilla Public License
+ * http://erik.eae.net/simplehtmlparser/simplehtmlparser.js
+ *
+ * ----------------------------------------------------------------------------
+ * License
+ * ----------------------------------------------------------------------------
+ *
+ * This code is triple licensed using Apache Software License 2.0,
+ * Mozilla Public License or GNU Public License
+ *
+ * ////////////////////////////////////////////////////////////////////////////
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License.  You may obtain a copy
+ * of the License at http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * ////////////////////////////////////////////////////////////////////////////
+ *
+ * The contents of this file are subject to the Mozilla Public License
+ * Version 1.1 (the "License"); you may not use this file except in
+ * compliance with the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS"
+ * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the
+ * License for the specific language governing rights and limitations
+ * under the License.
+ *
+ * The Original Code is Simple HTML Parser.
+ *
+ * The Initial Developer of the Original Code is Erik Arvidsson.
+ * Portions created by Erik Arvidssson are Copyright (C) 2004. All Rights
+ * Reserved.
+ *
+ * ////////////////////////////////////////////////////////////////////////////
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ *
+ * ----------------------------------------------------------------------------
+ * Usage
+ * ----------------------------------------------------------------------------
+ *
+ * // Use like so:
+ * HTMLParser(htmlString, {
+ *     start: function(tag, attrs, unary) {},
+ *     end: function(tag) {},
+ *     chars: function(text) {},
+ *     comment: function(text) {}
+ * });
+ *
+ * // or to get an XML string:
+ * HTMLtoXML(htmlString);
+ *
+ * // or to get an XML DOM Document
+ * HTMLtoDOM(htmlString);
+ *
+ * // or to inject into an existing document/DOM node
+ * HTMLtoDOM(htmlString, document);
+ * HTMLtoDOM(htmlString, document.body);
+ *
+ */
+// Regular Expressions for parsing tags and attributes
+var startTag = /^<([-A-Za-z0-9_]+)((?:\s+[a-zA-Z_:][-a-zA-Z0-9_:.]*(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)>/;
+var endTag = /^<\/([-A-Za-z0-9_]+)[^>]*>/;
+var attr = /([a-zA-Z_:][-a-zA-Z0-9_:.]*)(?:\s*=\s*(?:(?:"((?:\\.|[^"])*)")|(?:'((?:\\.|[^'])*)')|([^>\s]+)))?/g; // Empty Elements - HTML 5
+
+var empty = makeMap('area,base,basefont,br,col,frame,hr,img,input,link,meta,param,embed,command,keygen,source,track,wbr'); // Block Elements - HTML 5
+// fixed by xxx 将 ins 标签从块级名单中移除
+
+var block = makeMap('a,address,article,applet,aside,audio,blockquote,button,canvas,center,dd,del,dir,div,dl,dt,fieldset,figcaption,figure,footer,form,frameset,h1,h2,h3,h4,h5,h6,header,hgroup,hr,iframe,isindex,li,map,menu,noframes,noscript,object,ol,output,p,pre,section,script,table,tbody,td,tfoot,th,thead,tr,ul,video'); // Inline Elements - HTML 5
+
+var inline = makeMap('abbr,acronym,applet,b,basefont,bdo,big,br,button,cite,code,del,dfn,em,font,i,iframe,img,input,ins,kbd,label,map,object,q,s,samp,script,select,small,span,strike,strong,sub,sup,textarea,tt,u,var'); // Elements that you can, intentionally, leave open
+// (and which close themselves)
+
+var closeSelf = makeMap('colgroup,dd,dt,li,options,p,td,tfoot,th,thead,tr'); // Attributes that have their values filled in disabled="disabled"
+
+var fillAttrs = makeMap('checked,compact,declare,defer,disabled,ismap,multiple,nohref,noresize,noshade,nowrap,readonly,selected'); // Special Elements (can contain anything)
+
+var special = makeMap('script,style');
+function HTMLParser(html, handler) {
+  var index;
+  var chars;
+  var match;
+  var stack = [];
+  var last = html;
+
+  stack.last = function () {
+    return this[this.length - 1];
+  };
+
+  while (html) {
+    chars = true; // Make sure we're not in a script or style element
+
+    if (!stack.last() || !special[stack.last()]) {
+      // Comment
+      if (html.indexOf('<!--') == 0) {
+        index = html.indexOf('-->');
+
+        if (index >= 0) {
+          if (handler.comment) {
+            handler.comment(html.substring(4, index));
+          }
+
+          html = html.substring(index + 3);
+          chars = false;
+        } // end tag
+
+      } else if (html.indexOf('</') == 0) {
+        match = html.match(endTag);
+
+        if (match) {
+          html = html.substring(match[0].length);
+          match[0].replace(endTag, parseEndTag);
+          chars = false;
+        } // start tag
+
+      } else if (html.indexOf('<') == 0) {
+        match = html.match(startTag);
+
+        if (match) {
+          html = html.substring(match[0].length);
+          match[0].replace(startTag, parseStartTag);
+          chars = false;
+        }
+      }
+
+      if (chars) {
+        index = html.indexOf('<');
+        var text = index < 0 ? html : html.substring(0, index);
+        html = index < 0 ? '' : html.substring(index);
+
+        if (handler.chars) {
+          handler.chars(text);
+        }
+      }
+    } else {
+      html = html.replace(new RegExp('([\\s\\S]*?)<\/' + stack.last() + '[^>]*>'), function (all, text) {
+        text = text.replace(/<!--([\s\S]*?)-->|<!\[CDATA\[([\s\S]*?)]]>/g, '$1$2');
+
+        if (handler.chars) {
+          handler.chars(text);
+        }
+
+        return '';
+      });
+      parseEndTag('', stack.last());
+    }
+
+    if (html == last) {
+      throw 'Parse Error: ' + html;
+    }
+
+    last = html;
+  } // Clean up any remaining tags
+
+
+  parseEndTag();
+
+  function parseStartTag(tag, tagName, rest, unary) {
+    tagName = tagName.toLowerCase();
+    if (block[tagName]) {
+      while (stack.last() && inline[stack.last()]) {
+        parseEndTag('', stack.last());
+      }
+    }
+
+    if (closeSelf[tagName] && stack.last() == tagName) {
+      parseEndTag('', tagName);
+    }
+
+    unary = empty[tagName] || !!unary;
+
+    if (!unary) {
+      stack.push(tagName);
+    }
+
+    if (handler.start) {
+      var attrs = [];
+      rest.replace(attr, function (match, name) {
+        var value = arguments[2] ? arguments[2] : arguments[3] ? arguments[3] : arguments[4] ? arguments[4] : fillAttrs[name] ? name : '';
+        attrs.push({
+          name: name,
+          value: value,
+          escaped: value.replace(/(^|[^\\])"/g, '$1\\\"') // "
+
+        });
+      });
+
+      if (handler.start) {
+        handler.start(tagName, attrs, unary);
+      }
+    }
+  }
+
+  function parseEndTag(tag, tagName) {
+    // If no tag name is provided, clean shop
+    if (!tagName) {
+      var pos = 0;
+    } // Find the closest opened tag of the same type
+    else {
+        for (var pos = stack.length - 1; pos >= 0; pos--) {
+          if (stack[pos] == tagName) {
+            break;
+          }
+        }
+      }
+
+    if (pos >= 0) {
+      // Close all the open elements, up the stack
+      for (var i = stack.length - 1; i >= pos; i--) {
+        if (handler.end) {
+          handler.end(stack[i]);
+        }
+      } // Remove the open elements from the stack
+
+
+      stack.length = pos;
+    }
+  }
+}
+
+function makeMap(str) {
+  var obj = {};
+  var items = str.split(',');
+
+  for (var i = 0; i < items.length; i++) {
+    obj[items[i]] = true;
+  }
+
+  return obj;
+}
+
+function removeDOCTYPE(html) {
+  return html.replace(/<\?xml.*\?>\n/, '').replace(/<!doctype.*>\n/, '').replace(/<!DOCTYPE.*>\n/, '');
+}
+
+function parseAttrs(attrs) {
+  return attrs.reduce(function (pre, attr) {
+    var value = attr.value;
+    var name = attr.name;
+    if (pre[name]) {
+			pre[name] = pre[name] + " " + value;
+    } else {
+			pre[name] = value;
+    }
+
+    return pre;
+  }, {});
+}
+function convertStyleStringToJSON(styleString) {
+  var styles = styleString.split(";"); // 通过分号将样式字符串分割为多个样式声明
+  var result = {};
+
+  styles.forEach(function(style) {
+    var styleParts = style.split(":"); // 通过冒号将样式声明分割为属性和值
+    var property = styleParts[0].trim();
+    var value = styleParts[1] && styleParts[1].trim();
+
+    if (property && value) {
+      result[property] = value; // 将属性和值添加到结果对象中
+    }
+  });
+
+  return result;
+}
+function parseHtml(html) {
+  html = removeDOCTYPE(html);
+  var stacks = [];
+  var results = {
+    node: 'root',
+    children: []
+  };
+  HTMLParser(html, {
+    start: function start(tag, attrs, unary) {
+      var node = {
+        name: tag
+      };
+
+      if (attrs.length !== 0) {
+        node.attrs = parseAttrs(attrs);
+		node.styles = node.attrs.style ? convertStyleStringToJSON(node.attrs.style) : {}
+      }
+	
+	if(!node.type) {
+	  if(inline[node.name] && node.name !== 'img' ) {
+		node.type = 'text';
+		if(node.name == 'br') {
+			node.text = '\n'
+		} else if(node.name == 'strong'){
+			node.styles.fontWeight = 'bold'
+		}
+	  } else if(node.name == 'img'){
+		 node.type = 'image' 
+		 node.src =  node.attrs.src
+	  } else {
+		   node.type = 'view' 
+		   if(['h1','h2','h3','h4','h5','h6'].includes(node.name)) {
+			   node.styles.fontWeight = 'bold'
+		   }
+	  }
+	}		
+      if (unary) {
+        var parent = stacks[0] || results;
+
+        if (!parent.children) {
+          parent.children = [];
+        }
+
+        parent.children.push(node);
+      } else {
+        stacks.unshift(node);
+      }
+    },
+    end: function end(tag) {
+      var node = stacks.shift();
+      if (node.name !== tag) console.error('invalid state: mismatch end tag');
+      if (stacks.length === 0) {
+        results.children.push(node);
+      } else {
+        var parent = stacks[0];
+
+        if (!parent.children) {
+          parent.children = [];
+        }
+        parent.children.push(node);
+      }
+	  const isTextBox = node.children && node.children.length > 1 && node.children.every(child => {
+		  return ['text','image'].includes(child.type)
+	  })
+	  if(isTextBox) {
+		  node.type = 'textBox'
+	  }
+    },
+    chars: function chars(text) {
+      var node = {
+        type: 'text',
+        text: text
+      };
+
+      if (stacks.length === 0) {
+        results.children.push(node);
+      } else {
+        var parent = stacks[0];
+
+        if (!parent.children) {
+          parent.children = [];
+        }
+
+        parent.children.push(node);
+      }
+    },
+    comment: function comment(text) {
+      var node = {
+        node: 'comment',
+        text: text
+      };
+      var parent = stacks[0];
+
+      if (!parent.children) {
+        parent.children = [];
+      }
+
+      parent.children.push(node);
+    }
+  });
+  return results.children;
+}
+
+export default parseHtml;

+ 963 - 0
uni_modules/lime-painter/readme.md

@@ -0,0 +1,963 @@
+# Painter 画板 测试版
+
+> uniapp 海报画板,更优雅的海报生成方案  
+> [查看更多 站点 1](https://limeui.qcoon.cn/#/painter)  
+> [查看更多 站点 2](http://liangei.gitee.io/limeui/#/painter)  
+> Q 群:1169785031
+
+## 平台兼容
+
+| H5  | 微信小程序 | 支付宝小程序 | 百度小程序 | 头条小程序 | QQ 小程序 | App |
+| --- | ---------- | ------------ | ---------- | ---------- | --------- | --- |
+| √   | √          | √            | 未测       | √          | √         | √   |
+
+## 安装
+在市场导入**[海报画板](https://ext.dcloud.net.cn/plugin?id=2389)uni_modules**版本的即可,无需`import`
+
+## 代码演示
+
+### 插件demo
+- lime-painter 为 demo
+- 位于 uni_modules/lime-painter/components/lime-painter
+- 导入插件后直接使用可查看demo
+```vue
+<lime-painter />
+```
+
+
+### 基本用法
+
+- 插件提供 JSON 及 Template 的方式绘制海报
+- 参考 css 块状流布局模拟 css schema。
+- 另外flex布局还不是成完善,请谨慎使用,普通的流布局我觉得已经够用了。
+
+#### 方式一 Template
+
+- 提供`l-painter-view`、`l-painter-text`、`l-painter-image`、`l-painter-qrcode`四种类型组件
+- 通过 `css` 属性绘制样式,与 style 使用方式保持一致。
+```html
+<l-painter>
+	//如果使用Template出现顺序错乱,可使用`template` 等所有变量完成再显示
+	<template v-if="show">
+		<l-painter-view
+			css="background: #07c160; height: 120rpx; width: 120rpx; display: inline-block"
+		></l-painter-view>
+		<l-painter-view
+			css="background: #1989fa; height: 120rpx; width: 120rpx; border-top-right-radius: 60rpx; border-bottom-left-radius: 60rpx; display: inline-block; margin: 0 30rpx;"
+		></l-painter-view>
+		<l-painter-view
+			css="background: #ff9d00; height: 120rpx; width: 120rpx; border-radius: 50%; display: inline-block"
+		></l-painter-view>
+	<template>
+</l-painter>
+```
+
+#### 方式二 JSON
+
+- 在 json 里四种类型组件的`type`为`view`、`text`、`image`、`qrcode`
+- 通过 `board` 设置海报所需的 JSON 数据进行绘制或`ref`获取组件实例调用组件内的`render(json)`
+- 所有类型的 schema 都具有`css`字段,css 的 key 值使用**驼峰**如:`lineHeight`
+
+```html
+<l-painter :board="poster"/>
+```
+
+```js
+data() {
+	return {
+		poster: {
+			css: {
+				// 根节点若无尺寸,自动获取父级节点
+				width: '750rpx'
+			},
+			views: [
+				{
+					css: {
+						background: "#07c160",
+						height: "120rpx",
+						width: "120rpx",
+						display: "inline-block"
+					},
+					type: "view"
+				},
+				{
+					css: {
+						background: "#1989fa",
+						height: "120rpx",
+						width: "120rpx",
+						borderTopRightRadius: "60rpx",
+						borderBottomLeftRadius: "60rpx",
+						display: "inline-block",
+						margin: "0 30rpx"
+					},
+					views: [],
+					type: "view"
+				},
+				{
+					css: {
+						background: "#ff9d00",
+						height: "120rpx",
+						width: "120rpx",
+						borderRadius: "50%",
+						display: "inline-block"
+					},
+					views: [],
+					type: "view"
+				},
+			]
+		}
+	}
+}
+```
+
+### View 容器
+
+- 类似于 `div` 可以嵌套承载更多的 view、text、image,qrcode 共同构建一颗完整的节点树
+- 在 JSON 里具有 `views` 的数组字段,用于嵌套承载节点。
+
+#### 方式一 Template
+
+```html
+<l-painter>
+  <l-painter-view css="background: #f0f0f0; padding-top: 100rpx;">
+    <l-painter-view
+      css="background: #d9d9d9; width: 33.33%; height: 100rpx; display: inline-block"
+    ></l-painter-view>
+    <l-painter-view
+      css="background: #bfbfbf; width: 66.66%; height: 100rpx; display: inline-block"
+    ></l-painter-view>
+  </l-painter-view>
+</l-painter>
+```
+
+#### 方式二 JSON
+
+```js
+{
+	css: {},
+	views: [
+		{
+			type: 'view',
+			css: {
+				background: '#f0f0f0',
+				paddingTop: '100rpx'
+			},
+			views: [
+				{
+					type: 'view',
+					css: {
+						background: '#d9d9d9',
+						width: '33.33%',
+						height: '100rpx',
+						display: 'inline-block'
+					}
+				},
+				{
+					type: 'view',
+					css: {
+						background: '#bfbfbf',
+						width: '66.66%',
+						height: '100rpx',
+						display: 'inline-block'
+					}
+				}
+			],
+
+		}
+	]
+}
+```
+
+### Text 文本
+
+- 通过 `text` 属性填写文本内容。
+- 支持`\n`换行符
+- 支持省略号,使用 css 的`line-clamp`设置行数,当文字内容超过会显示省略号。
+- 支持`text-decoration`
+
+#### 方式一 Template
+
+```html
+<l-painter>
+  <l-painter-view css="background: #e0e2db; padding: 30rpx; color: #222a29">
+    <l-painter-text
+      text="登鹳雀楼\n白日依山尽,黄河入海流\n欲穷千里目,更上一层楼"
+    />
+    <l-painter-text
+      text="登鹳雀楼\n白日依山尽,黄河入海流\n欲穷千里目,更上一层楼"
+      css="text-align:center; padding-top: 20rpx; text-decoration: line-through "
+    />
+    <l-painter-text
+      text="登鹳雀楼\n白日依山尽,黄河入海流\n欲穷千里目,更上一层楼"
+      css="text-align:right; padding-top: 20rpx"
+    />
+    <l-painter-text
+      text="水调歌头\n明月几时有?把酒问青天。不知天上宫阙,今夕是何年。我欲乘风归去,又恐琼楼玉宇,高处不胜寒。起舞弄清影,何似在人间。"
+      css="line-clamp: 3; padding-top: 20rpx; background: linear-gradient(,#ff971b 0%, #ff5000 100%); background-clip: text"
+    />
+  </l-painter-view>
+</l-painter>
+```
+
+#### 方式二 JSON
+
+```js
+// 基础用法
+{
+	type: 'text',
+	text: '登鹳雀楼\n白日依山尽,黄河入海流\n欲穷千里目,更上一层楼',
+},
+{
+	type: 'text',
+	text: '登鹳雀楼\n白日依山尽,黄河入海流\n欲穷千里目,更上一层楼',
+	css: {
+		// 设置居中对齐
+		textAlign: 'center',
+		// 设置中划线
+		textDecoration: 'line-through'
+	}
+},
+{
+	type: 'text',
+	text: '登鹳雀楼\n白日依山尽,黄河入海流\n欲穷千里目,更上一层楼',
+	css: {
+		// 设置右对齐
+		textAlign: 'right',
+	}
+},
+{
+	type: 'text',
+	text: '登鹳雀楼\n白日依山尽,黄河入海流\n欲穷千里目,更上一层楼',
+	css: {
+		// 设置行数,超出显示省略号
+		lineClamp: 3,
+		// 渐变文字
+		background: 'linear-gradient(,#ff971b 0%, #1989fa 100%)',
+		backgroundClip: 'text'
+	}
+}
+```
+
+### Image 图片
+
+- 通过 `src` 属性填写图片路径。
+- 图片路径支持:网络图片,本地 static 里的图片路径,缓存路径,**字节的static目录是写相对路径**
+- 通过 `css` 的 `object-fit`属性可以设置图片的填充方式,可选值见下方 CSS 表格。
+- 通过 `css` 的 `object-position`配合 `object-fit` 可以设置图片的对齐方式,类似于`background-position`,详情见下方 CSS 表格。
+- 使用网络图片时:小程序需要去公众平台配置 [downloadFile](https://mp.weixin.qq.com/) 域名
+- 使用网络图片时:**H5 和 Nvue 需要决跨域问题**
+
+#### 方式一 Template
+
+```html
+<l-painter>
+  <!-- 基础用法 -->
+  <l-painter-image
+    src="https://m.360buyimg.com/babel/jfs/t1/196317/32/13733/288158/60f4ea39E6fb378ed/d69205b1a8ed3c97.jpg"
+    css="width: 200rpx; height: 200rpx"
+  />
+  <!-- 填充方式 -->
+  <!-- css object-fit 设置 填充方式 见下方表格-->
+  <l-painter-image
+    src="https://m.360buyimg.com/babel/jfs/t1/196317/32/13733/288158/60f4ea39E6fb378ed/d69205b1a8ed3c97.jpg"
+    css="width: 200rpx; height: 200rpx; object-fit: contain; background: #eee"
+  />
+  <!-- css object-position 设置 图片的对齐方式-->
+  <l-painter-image
+    src="https://m.360buyimg.com/babel/jfs/t1/196317/32/13733/288158/60f4ea39E6fb378ed/d69205b1a8ed3c97.jpg"
+    css="width: 200rpx; height: 200rpx; object-fit: contain; object-position: 50% 50%; background: #eee"
+  />
+</l-painter>
+```
+
+#### 方式二 JSON
+
+```js
+// 基础用法
+{
+	type: 'image',
+	src: 'https://m.360buyimg.com/babel/jfs/t1/196317/32/13733/288158/60f4ea39E6fb378ed/d69205b1a8ed3c97.jpg',
+	css: {
+		width: '200rpx',
+		height: '200rpx'
+	}
+},
+// 填充方式
+// css objectFit 设置 填充方式 见下方表格
+{
+	type: 'image',
+	src: 'https://m.360buyimg.com/babel/jfs/t1/196317/32/13733/288158/60f4ea39E6fb378ed/d69205b1a8ed3c97.jpg',
+	css: {
+		width: '200rpx',
+		height: '200rpx',
+		objectFit: 'contain'
+	}
+},
+// css objectPosition 设置 图片的对齐方式
+{
+	type: 'image',
+	src: 'https://m.360buyimg.com/babel/jfs/t1/196317/32/13733/288158/60f4ea39E6fb378ed/d69205b1a8ed3c97.jpg',
+	css: {
+		width: '200rpx',
+		height: '200rpx',
+		objectFit: 'contain',
+		objectPosition: '50% 50%'
+	}
+}
+```
+
+### Qrcode 二维码
+
+- 通过`text`属性填写需要生成二维码的文本。
+- 通过 `css` 里的 `color` 可设置生成码点的颜色。
+- 通过 `css` 里的 `background`可设置背景色。
+- 通过 `css `里的 `width`、`height`设置尺寸。
+
+#### 方式一 Template
+
+```html
+<l-painter>
+  <l-painter-qrcode
+    text="limeui.qcoon.cn"
+    css="width: 200rpx; height: 200rpx"
+  />
+</l-painter>
+```
+
+#### 方式二 JSON
+
+```js
+{
+	type: 'qrcode',
+	text: 'limeui.qcoon.cn',
+	css: {
+		width: '200rpx',
+		height: '200rpx',
+	}
+}
+```
+
+### 富文本
+- 这是一个有限支持的测试能力,只能通过JSON方式,不要抱太大希望!
+- 首先需要把富文本转成JSON,这需要引入`parser`这个包,如果你不使用是不会进入主包
+
+```html
+<l-painter ref="painter"/>
+```
+```js
+import parseHtml from '@/uni_modules/lime-painter/parser'
+const json = parseHtml(`<p><span>测试测试</span><img src="/static/logo.png"/></p>`)
+this.$refs.painter.render(json)
+```
+
+### 生成图片
+
+- 方式1、通过设置`isCanvasToTempFilePath`自动生成图片并在 `@success` 事件里接收海报临时路径
+- 方式2、通过调用内部方法生成图片:
+
+```html
+<l-painter ref="painter">...code</l-painter>
+```
+
+```js
+this.$refs.painter.canvasToTempFilePathSync({
+  fileType: "jpg",
+  // 如果返回的是base64是无法使用 saveImageToPhotosAlbum,需要设置 pathType为url
+  pathType: 'url',
+  quality: 1,
+  success: (res) => {
+    console.log(res.tempFilePath);
+	// 非H5 保存到相册
+	// H5 提示用户长按图另存
+	uni.saveImageToPhotosAlbum({
+		filePath: res.tempFilePath,
+		success: function () {
+			console.log('save success');
+		}
+	});
+  },
+});
+```
+
+### 主动调用方式
+
+- 通过获取组件实例内部的`render`函数 传递`JSON`即可
+
+```html
+<l-painter ref="painter" />
+```
+
+```js
+// 渲染
+this.$refs.painter.render(jsonSchema);
+// 生成图片
+this.$refs.painter.canvasToTempFilePathSync({
+  fileType: "jpg",
+  // 如果返回的是base64是无法使用 saveImageToPhotosAlbum,需要设置 pathType为url
+  pathType: 'url',
+  quality: 1,
+  success: (res) => {
+    console.log(res.tempFilePath);
+	// 非H5 保存到相册
+	uni.saveImageToPhotosAlbum({
+		filePath: res.tempFilePath,
+		success: function () {
+			console.log('save success');
+		}
+	});
+  },
+});
+```
+
+
+### H5跨域
+- 一般是需要后端或管理OSS资源的大佬处理
+- 一般OSS的处理方式:
+
+1、设置来源
+```cmd
+*
+```
+
+2、允许Methods
+```html
+GET
+```
+
+3、允许Headers
+```html
+access-control-allow-origin:*
+```
+
+4、最后如果还是不行,可试下给插件设置`useCORS`
+```html
+<l-painter useCORS>
+```
+
+
+
+### 海报示例
+
+- 提供一份示例,只把插件当成生成图片的工具,非必要不要在弹窗里使用。
+- 通过设置`isCanvasToTempFilePath`主动生成图片,再由 `@success` 事件接收海报临时路径
+- 设置`hidden`隐藏画板。
+请注意,示例用到了图片,海报的渲染是包括下载图片的时间,也许在某天图片会失效或访问超级慢,请更换为你的图片再查看,另外如果你是小程序请在使用示例时把**不校验合法域名**勾上!!!!!不然不显示还以为是插件的锅,求求了大佬们!
+#### 方式一 Template
+
+```html
+<image :src="path" mode="widthFix"></image>
+<l-painter
+  isCanvasToTempFilePath
+  @success="path = $event"
+  hidden
+  css="width: 750rpx; padding-bottom: 40rpx; background: linear-gradient(,#ff971b 0%, #ff5000 100%)"
+>
+  <l-painter-image
+    src="https://m.360buyimg.com/babel/jfs/t1/196317/32/13733/288158/60f4ea39E6fb378ed/d69205b1a8ed3c97.jpg"
+    css="margin-left: 40rpx; margin-top: 40rpx; width: 84rpx;  height: 84rpx; border-radius: 50%;"
+  />
+  <l-painter-view
+    css="margin-top: 40rpx; padding-left: 20rpx; display: inline-block"
+  >
+    <l-painter-text
+      text="隔壁老王"
+      css="display: block; padding-bottom: 10rpx; color: #fff; font-size: 32rpx; fontWeight: bold"
+    />
+    <l-painter-text
+      text="为您挑选了一个好物"
+      css="color: rgba(255,255,255,.7); font-size: 24rpx"
+    />
+  </l-painter-view>
+  <l-painter-view
+    css="margin-left: 40rpx; margin-top: 30rpx; padding: 32rpx; box-sizing: border-box; background: #fff; border-radius: 16rpx; width: 670rpx; box-shadow: 0 20rpx 58rpx rgba(0,0,0,.15)"
+  >
+    <l-painter-image
+      src="https://m.360buyimg.com/babel/jfs/t1/196317/32/13733/288158/60f4ea39E6fb378ed/d69205b1a8ed3c97.jpg"
+      css="object-fit: cover; object-position: 50% 50%; width: 606rpx; height: 606rpx; border-radius: 12rpx;"
+    />
+    <l-painter-view
+      css="margin-top: 32rpx; color: #FF0000; font-weight: bold; font-size: 28rpx; line-height: 1em;"
+    >
+      <l-painter-text text="¥" css="vertical-align: bottom" />
+      <l-painter-text
+        text="39"
+        css="vertical-align: bottom; font-size: 58rpx"
+      />
+      <l-painter-text text=".39" css="vertical-align: bottom" />
+      <l-painter-text
+        text="¥59.99"
+        css="vertical-align: bottom; padding-left: 10rpx; font-weight: normal; text-decoration: line-through; color: #999999"
+      />
+    </l-painter-view>
+    <l-painter-view css="margin-top: 32rpx; font-size: 26rpx; color: #8c5400">
+      <l-painter-text text="自营" css="color: #212121; background: #ffb400;" />
+      <l-painter-text
+        text="30天最低价"
+        css="margin-left: 16rpx; background: #fff4d9; text-decoration: line-through;"
+      />
+      <l-painter-text
+        text="满减优惠"
+        css="margin-left: 16rpx; background: #fff4d9"
+      />
+      <l-painter-text
+        text="超高好评"
+        css="margin-left: 16rpx; background: #fff4d9"
+      />
+    </l-painter-view>
+    <l-painter-view css="margin-top: 30rpx">
+      <l-painter-text
+        css="line-clamp: 2; color: #333333; line-height: 1.8em; font-size: 36rpx; width: 478rpx; padding-right:32rpx; box-sizing: border-box"
+        text="360儿童电话手表9X 智能语音问答定位支付手表 4G全网通20米游泳级防水视频通话拍照手表男女孩星空蓝"
+      ></l-painter-text>
+      <l-painter-qrcode
+        css="width: 128rpx; height: 128rpx;"
+        text="limeui.qcoon.cn"
+      ></l-painter-qrcode>
+    </l-painter-view>
+  </l-painter-view>
+</l-painter>
+```
+
+```js
+data() {
+	return {
+		path: ''
+	}
+}
+```
+
+#### 方式二 JSON
+
+```html
+<image :src="path" mode="widthFix"></image>
+<l-painter
+  :board="poster"
+  isCanvasToTempFilePath
+  @success="path = $event"
+  hidden
+/>
+```
+
+```js
+data() {
+	return {
+		path: '',
+		poster: {
+		    css: {
+		        width: "750rpx",
+		        paddingBottom: "40rpx",
+		        background: "linear-gradient(,#000 0%, #ff5000 100%)"
+		    },
+		    views: [
+		        {
+		            src: "https://m.360buyimg.com/babel/jfs/t1/196317/32/13733/288158/60f4ea39E6fb378ed/d69205b1a8ed3c97.jpg",
+		            type: "image",
+		            css: {
+		                background: "#fff",
+		                objectFit: "cover",
+		                marginLeft: "40rpx",
+		                marginTop: "40rpx",
+		                width: "84rpx",
+		                border: "2rpx solid #fff",
+		                boxSizing: "border-box",
+		                height: "84rpx",
+		                borderRadius: "50%"
+		            }
+		        },
+		        {
+		            type: "view",
+		            css: {
+		                marginTop: "40rpx",
+		                paddingLeft: "20rpx",
+		                display: "inline-block"
+		            },
+		            views: [
+		                {
+		                    text: "隔壁老王",
+		                    type: "text",
+		                    css: {
+		                        display: "block",
+		                        paddingBottom: "10rpx",
+		                        color: "#fff",
+		                        fontSize: "32rpx",
+		                        fontWeight: "bold"
+		                    }
+		                },
+		                {
+		                    text: "为您挑选了一个好物",
+		                    type: "text",
+		                    css: {
+		                        color: "rgba(255,255,255,.7)",
+		                        fontSize: "24rpx"
+		                    },
+		                }
+		            ],
+		        },
+		        {
+		            css: {
+		                marginLeft: "40rpx",
+		                marginTop: "30rpx",
+		                padding: "32rpx",
+		                boxSizing: "border-box",
+		                background: "#fff",
+		                borderRadius: "16rpx",
+		                width: "670rpx",
+		                boxShadow: "0 20rpx 58rpx rgba(0,0,0,.15)"
+		            },
+		            views: [
+		                {
+							src: "https://m.360buyimg.com/babel/jfs/t1/196317/32/13733/288158/60f4ea39E6fb378ed/d69205b1a8ed3c97.jpg",
+							type: "image",
+		                    css: {
+		                        objectFit: "cover",
+		                        objectPosition: "50% 50%",
+		                        width: "606rpx",
+		                        height: "606rpx"
+		                    },
+		                }, {
+		                    css: {
+		                        marginTop: "32rpx",
+		                        color: "#FF0000",
+		                        fontWeight: "bold",
+		                        fontSize: "28rpx",
+		                        lineHeight: "1em"
+		                    },
+		                    views: [{
+								text: "¥",
+								type: "text",
+		                        css: {
+		                            verticalAlign: "bottom"
+		                        },
+		                    }, {
+								text: "39",
+								type: "text",
+		                        css: {
+		                            verticalAlign: "bottom",
+		                            fontSize: "58rpx"
+		                        },
+		                    }, {
+								text: ".39",
+								type: "text",
+		                        css: {
+		                            verticalAlign: "bottom"
+		                        },
+		                    }, {
+								text: "¥59.99",
+								type: "text",
+		                        css: {
+		                            verticalAlign: "bottom",
+		                            paddingLeft: "10rpx",
+		                            fontWeight: "normal",
+		                            textDecoration: "line-through",
+		                            color: "#999999"
+		                        }
+		                    }],
+
+		                    type: "view"
+		                }, {
+		                    css: {
+		                        marginTop: "32rpx",
+		                        fontSize: "26rpx",
+		                        color: "#8c5400"
+		                    },
+		                    views: [{
+								text: "自营",
+								type: "text",
+		                        css: {
+		                            color: "#212121",
+		                            background: "#ffb400"
+		                        },
+		                    }, {
+								text: "30天最低价",
+								type: "text",
+		                        css: {
+		                            marginLeft: "16rpx",
+		                            background: "#fff4d9",
+		                            textDecoration: "line-through"
+		                        },
+		                    }, {
+								text: "满减优惠",
+								type: "text",
+		                        css: {
+		                            marginLeft: "16rpx",
+		                            background: "#fff4d9"
+		                        },
+		                    }, {
+								text: "超高好评",
+								type: "text",
+		                        css: {
+		                            marginLeft: "16rpx",
+		                            background: "#fff4d9"
+		                        },
+
+		                    }],
+
+		                    type: "view"
+		                }, {
+		                    css: {
+		                        marginTop: "30rpx"
+		                    },
+		                    views: [
+								{
+									text: "360儿童电话手表9X 智能语音问答定位支付手表 4G全网通20米游泳级防水视频通话拍照手表男女孩星空蓝",
+									type: "text",
+									css: {
+										paddingRight: "32rpx",
+										boxSizing: "border-box",
+										lineClamp: 2,
+										color: "#333333",
+										lineHeight: "1.8em",
+										fontSize: "36rpx",
+										width: "478rpx"
+		                        },
+		                    }, {
+								text: "limeui.qcoon.cn",
+								type: "qrcode",
+		                        css: {
+		                            width: "128rpx",
+		                            height: "128rpx",
+		                        },
+
+		                    }],
+		                    type: "view"
+		                }],
+		            type: "view"
+		        }
+		    ]
+		}
+	}
+}
+```
+
+
+### 自定义字体
+- 需要平台的支持,已知微信小程序支持,其它的没试过,如果可行请告之
+
+```
+// 需要在app.vue中下载字体
+uni.loadFontFace({
+	global:true,
+	scopes: ['native'],
+	family: '自定义字体名称',
+	source: 'url("https://sungd.github.io/Pacifico.ttf")',
+  
+	success() {
+	  console.log('success')
+  }
+})
+
+
+// 然后就可以在插件的css中写font-family: '自定义字体名称'
+```
+
+
+### Nvue
+- 必须为HBX 3.4.11及以上
+
+
+### 原生小程序
+
+- 插件里的`painter.js`支持在原生小程序中使用
+- new Painter 之后在`source`里传入 JSON
+- 再调用`render`绘制海报
+- 如需生成图片,请查看微信小程序 cavnas 的[canvasToTempFilePath](https://developers.weixin.qq.com/miniprogram/dev/api/canvas/wx.canvasToTempFilePath.html)
+
+```html
+<canvas type="2d" id="painter" style="width: 100%"></canvas>
+```
+
+```js
+import { Painter } from "./painter";
+page({
+  data: {
+    poster: {
+      css: {
+        width: "750rpx",
+      },
+      views: [
+        {
+          type: "view",
+          css: {
+            background: "#d2d4c8",
+            paddingTop: "100rpx",
+          },
+          views: [
+            {
+              type: "view",
+              css: {
+                background: "#5f7470",
+                width: "33.33%",
+                height: "100rpx",
+                display: "inline-block",
+              },
+            },
+            {
+              type: "view",
+              css: {
+                background: "#889696",
+                width: "33.33%",
+                height: "100rpx",
+                display: "inline-block",
+              },
+            },
+            {
+              type: "view",
+              css: {
+                background: "#b8bdb5",
+                width: "33.33%",
+                height: "100rpx",
+                display: "inline-block",
+              },
+            },
+          ],
+        },
+      ],
+    },
+  },
+  async onLoad() {
+    const res = await this.getCentext();
+    const painter = new Painter(res);
+    // 返回计算布局后的整个内容尺寸
+    const { width, height } = await painter.source(this.data.poster);
+    // 得到计算后的尺寸后 可给canvas尺寸赋值,达到动态响应效果
+    // 渲染
+    await painter.render();
+  },
+  // 获取canvas 2d
+  // 非2d 需要传一个 createImage 方法用于获取图片信息 即把 getImageInfo 的 success 通过 promise resolve 返回
+  getCentext() {
+    return new Promise((resolve) => {
+      wx.createSelectorQuery()
+        .select(`#painter`)
+        .node()
+        .exec((res) => {
+          let { node: canvas } = res[0];
+          resolve({
+            canvas,
+            context: canvas.getContext("2d"),
+            width: canvas.width,
+            height: canvas.height,
+			// createImage: getImageInfo()
+            pixelRatio: 2,
+          });
+        });
+    });
+  },
+});
+```
+
+### 旧版(1.6.x)更新
+
+- 由于 1.8.x 版放弃了以定位的方式,所以 1.6.x 版更新之后要每个样式都加上`position: absolute`
+- 旧版的 `image` mode 模式被放弃,使用`object-fit`
+- 旧版的 `isRenderImage` 改成 `is-canvas-to-temp-file-path`
+- 旧版的 `maxLines` 改成 `line-clamp`
+
+## API
+
+### Props
+
+| 参数                       | 说明                                                         | 类型             | 默认值       |
+| -------------------------- | ------------------------------------------------------------ | ---------------- | ------------ |
+| board                      | JSON 方式的海报元素对象集                                    | <em>object</em>  | -            |
+| css                        | 海报内容最外层的样式,可以理解为`body`                           | <em>object</em>  | 参数请向下看 |
+| custom-style               | canvas 元素的样式                                            | <em>string</em>  |              |
+| hidden               		 | 隐藏画板                                                    | <em>boolean</em>  |   `false`    |
+| is-canvas-to-temp-file-path | 是否生成图片,在`@success`事件接收图片地址                   | <em>boolean</em> | `false`      |
+| after-delay                | 生成图片错乱,可延时生成图片                                 | <em>number</em>  | `100`        |
+| type                       | canvas 类型,对微信头条支付宝小程序可有效,可选值:`2d`,`''` | <em>string</em>  | `2d`         |
+| file-type                  | 生成图片的后缀类型, 可选值:`png`、`jpg`                     | <em>string</em>  | `png`        |
+| path-type                  | 生成图片路径类型,可选值`url`、`base64`                      | <em>string</em>  | `-`          |
+| pixel-ratio                | 生成图片的像素密度,默认为对应手机的像素密度,`nvue`无效     | <em>number</em>  | `-`          |
+| hidpi                | H5和APP是否使用高清处理     | <em>boolean</em>  | `true`          |
+| width                      | **废弃** 画板的宽度,一般只用于通过内部方法时加上            | <em>number</em>  | ``           |
+| height                     | **废弃** 画板的高度 ,同上                                   | <em>number</em>  | ``           |
+
+### css
+| 属性名                                                                              | 支持的值或类型                                                                                                                                                                       | 默认值   |
+| ----------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------- |
+| (min\max)width                                                                      | 支持`%`、`rpx`、`px`                                                                                                                                                                 | -        |
+| height                                                                              | 同上                                                                                                                                                                                 | -        |
+| color                                                                               | `string`                                                                                                                                                                             | -        |
+| position                                                                            | 定位,可选值:`absolute`、`fixed`                                                                                                                                                    | -        |
+| ↳ left、top、right、bottom                                                          | 配合`position`才生效,支持`%`、`rpx`、`px`                                                                                                                                           | -        |
+| margin                                                                              | 可简写或各方向分别写,如:`margin-top`,支持`auto`、`rpx`、`px`                                                                                                                      | -        |
+| padding                                                                             | 可简写或各方向分别写,支持`rpx`、`px`                                                                                                                                                | -        |
+| border                                                                              | 可简写或各个值分开写:`border-width`、`border-style` 、`border-color`,简写请按顺序写                                                                                                | -        |
+| line-clamp                                                                          | `number`,超过行数显示省略号                                                                                                                                                         | -        |
+| vertical-align                                                                      | 文字垂直对齐,可选值:`bottom`、`top`、`middle`                                                                                                                                      | `middle` |
+| line-height                                                                         | 文字行高,支持`rpx`、`px`、`em`                                                                                                                                                      | `1.4em`  |
+| font-weight                                                                         | 文字粗细,可选值:`normal`、`bold`                                                                                                                                                   | `normal` |
+| font-size                                                                           | 文字大小,`string`,支持`rpx`、`px`                                                                                                                                                  | `14px`   |
+| text-decoration                                                                     | 文本修饰,可选值:`underline` 、`line-through`、`overline`                                                                                                                           | -        |
+| text-stroke                                                                         | 文字描边,可简写或各个值分开写,如:`text-stroke-color`, `text-stroke-width`                                                                                                              | -        |
+| text-align                                                                          | 文本水平对齐,可选值:`right` 、`center`                                                                                                                                             | `left`   |
+| display                                                                             | 框类型,可选值:`block`、`inline-block`、`flex`、`none`,当为`none`时是不渲染该段, `flex`功能简陋。                                                                                                            | -        |
+| flex                                                                                | 配合 display: flex; 属性定义了在分配多余空间,目前只用为数值如: flex: 1                                                                                                           | -        |
+| align-self                                                                          | 配合 display: flex; 单个项目垂直轴对齐方式: `flex-start` `flex-end` `center`                                                                                                         | `flex-start`        |
+| justify-content                                                                     | 配合 display: flex; 水平轴对齐方式: `flex-start` `flex-end` `center`                                                                                                         | `flex-start`        |
+| align-items                                                                         | 配合 display: flex; 垂直轴对齐方式: `flex-start` `flex-end` `center`                                                                                                  | `flex-start`        |
+| border-radius                                                                       | 圆角边框,支持`%`、`rpx`、`px`                                                                                                                                                       | -        |
+| box-sizing                                                                          | 可选值:`border-box`                                                                                                                                                                 | -        |
+| box-shadow                                                                          | 投影                                                                                                                                                                                 | -        |
+| background(color)                                                                   | 支持渐变,但必须写百分比!如:`linear-gradient(,#ff971b 0%, #ff5000 100%)`、`radial-gradient(#0ff 15%, #f0f 60%)`,目前 radial-gradient 渐变的圆心为元素中点,半径为最长边,不支持设置 | -        |
+| background-clip                                                                	  | 文字渐变,配合`background`背景渐变,设置`background-clip: text` 达到文字渐变效果 | -        |
+| background-image                                                                    | view 元素背景:`url(src)`,若只是设置背景图,请不要设置`background-repeat`                                                                                                                                                           | -        |
+| background-repeat                                                                   | 设置是否及如何重复背景纹理,可选值:`repeat`、`repeat-x`、`repeat-y`、`no-repeat`                                                                                                    | `repeat` |
+| [object-fit](https://developer.mozilla.org/zh-CN/docs/Web/CSS/object-fit/)          | 图片元素适应容器方式,类似于`mode`,可选值:`cover`、 `contain`、 `fill`、 `none`                                                                                                      | -        |
+| [object-position](https://developer.mozilla.org/zh-CN/docs/Web/CSS/object-position) | 图片的对齐方式,配合`object-fit`使用                                                                                                                                                 | -        |
+
+### 图片填充模式 object-fit
+
+| 名称    | 含义                                                   |
+| ------- | ------------------------------------------------------ |
+| contain | 保持宽高缩放图片,使图片的长边能完全显示出来           |
+| cover   | 保持宽高缩放图片,使图片的短边能完全显示出来,裁剪长边 |
+| fill    | 拉伸图片,使图片填满元素                               |
+| none    | 保持图片原有尺寸                                       |
+
+### 事件 Events
+
+| 事件名   | 说明                                                             | 返回值 |
+| -------- | ---------------------------------------------------------------- | ------ |
+| success  | 生成图片成功,若使用`is-canvas-to-temp-filePath` 可以接收图片地址 | path   |
+| fail     | 生成图片失败                                                     | error  |
+| done     | 绘制成功                                                         |        |
+| progress | 绘制进度                                                         | number |
+
+### 暴露函数 Expose
+| 事件名   | 说明                                                             | 返回值 |
+| -------- | ---------------------------------------------------------------- | ------ |
+| render(object)   |  渲染器,传入JSON 绘制海报 | promise   |
+| [canvasToTempFilePath](https://uniapp.dcloud.io/api/canvas/canvasToTempFilePath.html#canvastotempfilepath)(object)   | 把当前画布指定区域的内容导出生成指定大小的图片,并返回文件临时路径。    |   |
+| canvasToTempFilePathSync(object)    | 同步接口,同上                                                         |        |
+
+
+## 常见问题
+
+- 1、H5 端使用网络图片需要解决跨域问题。
+- 2、小程序使用网络图片需要去公众平台增加下载白名单!二级域名也需要配!
+- 3、H5 端生成图片是 base64,有时显示只有一半可以使用原生标签`<IMG/>`
+- 4、发生保存图片倾斜变形或提示 native buffer exceed size limit 时,使用 pixel-ratio="2"参数,降分辨率。
+- 5、h5 保存图片不需要调接口,提示用户长按图片保存。
+- 6、画板不能隐藏,包括`v-if`,`v-show`、`display:none`、`opacity:0`,另外也不要把画板放在弹窗里。如果需要隐藏画板请设置 `custom-style="position: fixed; left: 200%"`
+- 7、微信小程序真机调试请使用 **真机调试2.0**,不支持1.0。
+- 8、微信小程序打开调试时可以生但并闭无法生成时,这种情况一般是没有在公众号配置download域名
+- 9、HBX 3.4.5之前的版本不支持vue3
+- 10、在微信开发工具上 canvas 层级最高无法zindex,并不影响真机
+- 11、请不要导入非uni_modules插件
+- 12、关于QQ小程序 报 Propertyor method"toJSON"is not defined 请把基础库调到 1.50.3
+- 13、支付宝小程序 IDE 不支持 生成图片 请以真机调试结果为准
+- 14、返回值为字符串 `data:,` 大概是尺寸超过限制,设置 pixel-ratio="2"
+- 华为手机 APP 上无法生成图片,请使用 HBX2.9.11++(已过时,忽略这条)
+- IOS APP 请勿使用 HBX2.9.3.20201014 的版本!这个版本无法生成图片。(已过时,忽略这条)
+- 苹果微信 7.0.20 存在闪退和图片无法 onload 为微信 bug(已过时,忽略这条)
+- 微信小程序 IOS 旧接口 如父级设置圆角,子级也设会导致子级的失效,为旧接口BUG。
+- 微信小程序 安卓 旧接口 如使用图片必须加背景色,为旧接口BUG。
+- 微信小程序 安卓端 [图片可能在首次可以加载成功,再次加载会不触发任何事件](https://developers.weixin.qq.com/community/develop/doc/000ee2b8dacf4009337f51f4556800?highLine=canvas%25202d%2520createImage),临时解决方法是给图片加个时间戳
+## 打赏
+
+如果你觉得本插件,解决了你的问题,赠人玫瑰,手留余香。
+
+![](https://testingcf.jsdelivr.net/gh/liangei/image@1.9/alipay.png)
+![](https://testingcf.jsdelivr.net/gh/liangei/image@1.9/wpay.png)

Some files were not shown because too many files changed in this diff