小程序自定义海报

news2024/10/7 9:16:24

如图微信小程序生成海报自定义调整位置

 

 

//微信小程序组件 poster.wxml

<view style='position: relative;{{customStyle}};{{painterStyle}}'>
  <block wx:if="{{!use2D}}">
    <canvas canvas-id="photo" style="{{photoStyle}};position: absolute; left: -9999px; top: -9999rpx;" />
    <block wx:if="{{dancePalette}}">
      <canvas canvas-id="bottom" style="{{painterStyle}};position: absolute;" />
      <canvas canvas-id="k-canvas" style="{{painterStyle}};position: absolute;" />
      <canvas canvas-id="top" style="{{painterStyle}};position: absolute;" />
      <canvas 
        canvas-id="front" 
        style="{{painterStyle}};position: absolute;"
        bindtouchstart="onTouchStart"
        bindtouchmove="onTouchMove"
        bindtouchend="onTouchEnd"
        bindtouchcancel="onTouchCancel"
        disable-scroll="{{true}}" />
      </block>
  </block>
  <block wx:if="{{use2D}}">
    <canvas type="2d" id="photo" style="{{photoStyle}};" />
  </block>
</view>
//组件poster.js

import Pen, { penCache, clearPenCache } from './lib/pen';
import Downloader from './lib/downloader';
import WxCanvas from './lib/wx-canvas';

const util = require('./lib/util');
const calc = require('./lib/calc');

const downloader = new Downloader();

// 最大尝试的绘制次数
const MAX_PAINT_COUNT = 5;
const ACTION_DEFAULT_SIZE = 24;
const ACTION_OFFSET = '2rpx';
Component({
  canvasWidthInPx: 0,
  canvasHeightInPx: 0,
  canvasNode: null,
  paintCount: 0,
  currentPalette: {},
  outterDisabled: false,
  isDisabled: false,
  needClear: false,
  /**
   * 组件的属性列表
   */
  properties: {
    use2D: {
      type: Boolean,
    },
    customStyle: {
      type: String,
    },
    // 运行自定义选择框和删除缩放按钮
    customActionStyle: {
      type: Object,
    },
    palette: {
      type: Object,
      observer: function (newVal, oldVal) {
        if (this.isNeedRefresh(newVal, oldVal)) {
          this.paintCount = 0;
          clearPenCache();
          this.startPaint();
        }
      },
    },
    dancePalette: {
      type: Object,
      observer: function (newVal, oldVal) {
        if (!this.isEmpty(newVal) && !this.properties.use2D) {
          clearPenCache();
          this.initDancePalette(newVal);
        }
      },
    },
    // 缩放比,会在传入的 palette 中统一乘以该缩放比
    scaleRatio: {
      type: Number,
      value: 1,
    },
    widthPixels: {
      type: Number,
      value: 0,
    },
    // 启用脏检查,默认 false
    dirty: {
      type: Boolean,
      value: false,
    },
    LRU: {
      type: Boolean,
      value: false,
    },
    action: {
      type: Object,
      observer: function (newVal, oldVal) {
        if (newVal && !this.isEmpty(newVal) && !this.properties.use2D) {
          this.doAction(newVal, null, false, true);
        }
      },
    },
    disableAction: {
      type: Boolean,
      observer: function (isDisabled) {
        this.outterDisabled = isDisabled;
        this.isDisabled = isDisabled;
      },
    },
    clearActionBox: {
      type: Boolean,
      observer: function (needClear) {
        if (needClear && !this.needClear) {
          if (this.frontContext) {
            setTimeout(() => {
              this.frontContext.draw();
            }, 100);
            this.touchedView = {};
            this.prevFindedIndex = this.findedIndex;
            this.findedIndex = -1;
          }
        }
        this.needClear = needClear;
      },
    },
  },

  data: {
    picURL: '',
    showCanvas: true,
    painterStyle: '',
  },

  methods: {
    /**
     * 判断一个 object 是否为 空
     * @param {object} object
     */
    isEmpty(object) {
      for (const i in object) {
        return false;
      }
      return true;
    },

    isNeedRefresh(newVal, oldVal) {
      if (!newVal || this.isEmpty(newVal) || (this.data.dirty && util.equal(newVal, oldVal))) {
        return false;
      }
      return true;
    },

    getBox(rect, type) {
      const boxArea = {
        type: 'rect',
        css: {
          height: `${rect.bottom - rect.top}px`,
          width: `${rect.right - rect.left}px`,
          left: `${rect.left}px`,
          top: `${rect.top}px`,
          borderWidth: '4rpx',
          borderColor: '#1A7AF8',
          color: 'transparent',
        },
      };
      if (type === 'text') {
        boxArea.css = Object.assign({}, boxArea.css, {
          borderStyle: 'dashed',
        });
      }
      if (this.properties.customActionStyle && this.properties.customActionStyle.border) {
        boxArea.css = Object.assign({}, boxArea.css, this.properties.customActionStyle.border);
      }
      Object.assign(boxArea, {
        id: 'box',
      });
      return boxArea;
    },

    getScaleIcon(rect, type) {
      let scaleArea = {};
      const { customActionStyle } = this.properties;
      if (customActionStyle && customActionStyle.scale) {
        scaleArea = {
          type: 'image',
          url: type === 'text' ? customActionStyle.scale.textIcon : customActionStyle.scale.imageIcon,
          css: {
            height: `${2 * ACTION_DEFAULT_SIZE}rpx`,
            width: `${2 * ACTION_DEFAULT_SIZE}rpx`,
            borderRadius: `${ACTION_DEFAULT_SIZE}rpx`,
          },
        };
      } else {
        scaleArea = {
          type: 'rect',
          css: {
            height: `${2 * ACTION_DEFAULT_SIZE}rpx`,
            width: `${2 * ACTION_DEFAULT_SIZE}rpx`,
            borderRadius: `${ACTION_DEFAULT_SIZE}rpx`,
            color: '#0000ff',
          },
        };
      }
      scaleArea.css = Object.assign({}, scaleArea.css, {
        align: 'center',
        left: `${rect.right + ACTION_OFFSET.toPx()}px`,
        top:
          type === 'text'
            ? `${rect.top - ACTION_OFFSET.toPx() - scaleArea.css.height.toPx() / 2}px`
            : `${rect.bottom - ACTION_OFFSET.toPx() - scaleArea.css.height.toPx() / 2}px`,
      });
      Object.assign(scaleArea, {
        id: 'scale',
      });
      return scaleArea;
    },

    getDeleteIcon(rect) {
      let deleteArea = {};
      const { customActionStyle } = this.properties;
      if (customActionStyle && customActionStyle.scale) {
        deleteArea = {
          type: 'image',
          url: customActionStyle.delete.icon,
          css: {
            height: `${2 * ACTION_DEFAULT_SIZE}rpx`,
            width: `${2 * ACTION_DEFAULT_SIZE}rpx`,
            borderRadius: `${ACTION_DEFAULT_SIZE}rpx`,
          },
        };
      } else {
        deleteArea = {
          type: 'rect',
          css: {
            height: `${2 * ACTION_DEFAULT_SIZE}rpx`,
            width: `${2 * ACTION_DEFAULT_SIZE}rpx`,
            borderRadius: `${ACTION_DEFAULT_SIZE}rpx`,
            color: '#0000ff',
          },
        };
      }
      deleteArea.css = Object.assign({}, deleteArea.css, {
        align: 'center',
        left: `${rect.left - ACTION_OFFSET.toPx()}px`,
        top: `${rect.top - ACTION_OFFSET.toPx() - deleteArea.css.height.toPx() / 2}px`,
      });
      Object.assign(deleteArea, {
        id: 'delete',
      });
      return deleteArea;
    },

    doAction(action, callback, isMoving, overwrite) {
      if (this.properties.use2D) {
        return;
      }
      let newVal = null;
      if (action) {
        newVal = action.view;
      }
      if (newVal && newVal.id && this.touchedView.id !== newVal.id) {
        // 带 id 的动作给撤回时使用,不带 id,表示对当前选中对象进行操作
        const { views } = this.currentPalette;
        for (let i = 0; i < views.length; i++) {
          if (views[i].id === newVal.id) {
            // 跨层回撤,需要重新构建三层关系
            this.touchedView = views[i];
            this.findedIndex = i;
            this.sliceLayers();
            break;
          }
        }
      }

      const doView = this.touchedView;

      if (!doView || this.isEmpty(doView)) {
        return;
      }
      if (newVal && newVal.css) {
        if (overwrite) {
          doView.css = newVal.css;
        } else if (Array.isArray(doView.css) && Array.isArray(newVal.css)) {
          doView.css = Object.assign({}, ...doView.css, ...newVal.css);
        } else if (Array.isArray(doView.css)) {
          doView.css = Object.assign({}, ...doView.css, newVal.css);
        } else if (Array.isArray(newVal.css)) {
          doView.css = Object.assign({}, doView.css, ...newVal.css);
        } else {
          doView.css = Object.assign({}, doView.css, newVal.css);
        }
      }
      if (newVal && newVal.rect) {
        doView.rect = newVal.rect;
      }
      if (newVal && newVal.url && doView.url && newVal.url !== doView.url) {
        downloader
          .download(newVal.url, this.properties.LRU)
          .then(path => {
            if (newVal.url.startsWith('https')) {
              doView.originUrl = newVal.url;
            }
            doView.url = path;
            wx.getImageInfo({
              src: path,
              success: res => {
                doView.sHeight = res.height;
                doView.sWidth = res.width;
                this.reDraw(doView, callback, isMoving);
              },
              fail: () => {
                this.reDraw(doView, callback, isMoving);
              },
            });
          })
          .catch(error => {
            // 未下载成功,直接绘制
            console.error(error);
            this.reDraw(doView, callback, isMoving);
          });
      } else {
        newVal && newVal.text && doView.text && newVal.text !== doView.text && (doView.text = newVal.text);
        newVal &&
          newVal.content &&
          doView.content &&
          newVal.content !== doView.content &&
          (doView.content = newVal.content);
        this.reDraw(doView, callback, isMoving);
      }
    },

    reDraw(doView, callback, isMoving) {
      const draw = {
        width: this.currentPalette.width,
        height: this.currentPalette.height,
        views: this.isEmpty(doView) ? [] : [doView],
      };
      const pen = new Pen(this.globalContext, draw);

      pen.paint(callbackInfo => {
        callback && callback(callbackInfo);
        this.triggerEvent('viewUpdate', {
          view: this.touchedView,
        });
      });

      const { rect, css, type } = doView;

      this.block = {
        width: this.currentPalette.width,
        height: this.currentPalette.height,
        views: this.isEmpty(doView) ? [] : [this.getBox(rect, doView.type)],
      };
      if (css && css.scalable) {
        this.block.views.push(this.getScaleIcon(rect, type));
      }
      if (css && css.deletable) {
        this.block.views.push(this.getDeleteIcon(rect));
      }
      const topBlock = new Pen(this.frontContext, this.block);
      topBlock.paint();
    },

    isInView(x, y, rect) {
      return x > rect.left && y > rect.top && x < rect.right && y < rect.bottom;
    },

    isInDelete(x, y) {
      for (const view of this.block.views) {
        if (view.id === 'delete') {
          return x > view.rect.left && y > view.rect.top && x < view.rect.right && y < view.rect.bottom;
        }
      }
      return false;
    },

    isInScale(x, y) {
      for (const view of this.block.views) {
        if (view.id === 'scale') {
          return x > view.rect.left && y > view.rect.top && x < view.rect.right && y < view.rect.bottom;
        }
      }
      return false;
    },

    touchedView: {},
    findedIndex: -1,
    onClick() {
      const x = this.startX;
      const y = this.startY;
      const totalLayerCount = this.currentPalette.views.length;
      let canBeTouched = [];
      let isDelete = false;
      let deleteIndex = -1;
      for (let i = totalLayerCount - 1; i >= 0; i--) {
        const view = this.currentPalette.views[i];
        const { rect } = view;
        if (this.touchedView && this.touchedView.id && this.touchedView.id === view.id && this.isInDelete(x, y, rect)) {
          canBeTouched.length = 0;
          deleteIndex = i;
          isDelete = true;
          break;
        }
        if (this.isInView(x, y, rect)) {
          canBeTouched.push({
            view,
            index: i,
          });
        }
      }
      this.touchedView = {};
      if (canBeTouched.length === 0) {
        this.findedIndex = -1;
      } else {
        let i = 0;
        const touchAble = canBeTouched.filter(item => Boolean(item.view.id));
        if (touchAble.length === 0) {
          this.findedIndex = canBeTouched[0].index;
        } else {
          for (i = 0; i < touchAble.length; i++) {
            if (this.findedIndex === touchAble[i].index) {
              i++;
              break;
            }
          }
          if (i === touchAble.length) {
            i = 0;
          }
          this.touchedView = touchAble[i].view;
          this.findedIndex = touchAble[i].index;
          this.triggerEvent('viewClicked', {
            view: this.touchedView,
          });
        }
      }
      if (this.findedIndex < 0 || (this.touchedView && !this.touchedView.id)) {
        // 证明点击了背景 或无法移动的view
        this.frontContext.draw();
        if (isDelete) {
          this.triggerEvent('touchEnd', {
            view: this.currentPalette.views[deleteIndex],
            index: deleteIndex,
            type: 'delete',
          });
          this.doAction();
        } else if (this.findedIndex < 0) {
          this.triggerEvent('viewClicked', {});
        }
        this.findedIndex = -1;
        this.prevFindedIndex = -1;
      } else if (this.touchedView && this.touchedView.id) {
        this.sliceLayers();
      }
    },

    sliceLayers() {
      const bottomLayers = this.currentPalette.views.slice(0, this.findedIndex);
      const topLayers = this.currentPalette.views.slice(this.findedIndex + 1);
      const bottomDraw = {
        width: this.currentPalette.width,
        height: this.currentPalette.height,
        background: this.currentPalette.background,
        views: bottomLayers,
      };
      const topDraw = {
        width: this.currentPalette.width,
        height: this.currentPalette.height,
        views: topLayers,
      };
      if (this.prevFindedIndex < this.findedIndex) {
        new Pen(this.bottomContext, bottomDraw).paint();
        this.doAction();
        new Pen(this.topContext, topDraw).paint();
      } else {
        new Pen(this.topContext, topDraw).paint();
        this.doAction();
        new Pen(this.bottomContext, bottomDraw).paint();
      }
      this.prevFindedIndex = this.findedIndex;
    },

    startX: 0,
    startY: 0,
    startH: 0,
    startW: 0,
    isScale: false,
    startTimeStamp: 0,
    onTouchStart(event) {
      if (this.isDisabled) {
        return;
      }
      const { x, y } = event.touches[0];
      this.startX = x;
      this.startY = y;
      this.startTimeStamp = new Date().getTime();
      if (this.touchedView && !this.isEmpty(this.touchedView)) {
        const { rect } = this.touchedView;
        if (this.isInScale(x, y, rect)) {
          this.isScale = true;
          this.startH = rect.bottom - rect.top;
          this.startW = rect.right - rect.left;
        } else {
          this.isScale = false;
        }
      } else {
        this.isScale = false;
      }
    },

    onTouchEnd(e) {
      if (this.isDisabled) {
        return;
      }
      const current = new Date().getTime();
      if (current - this.startTimeStamp <= 500 && !this.hasMove) {
        !this.isScale && this.onClick(e);
      } else if (this.touchedView && !this.isEmpty(this.touchedView)) {
        this.triggerEvent('touchEnd', {
          view: this.touchedView,
        });
      }
      this.hasMove = false;
    },

    onTouchCancel(e) {
      if (this.isDisabled) {
        return;
      }
      this.onTouchEnd(e);
    },

    hasMove: false,
    onTouchMove(event) {
      if (this.isDisabled) {
        return;
      }
      this.hasMove = true;
      if (!this.touchedView || (this.touchedView && !this.touchedView.id)) {
        return;
      }
      const { x, y } = event.touches[0];
      const offsetX = x - this.startX;
      const offsetY = y - this.startY;
      const { rect, type } = this.touchedView;
      let css = {};
      if (this.isScale) {
        clearPenCache(this.touchedView.id);
        const newW = this.startW + offsetX > 1 ? this.startW + offsetX : 1;
        if (this.touchedView.css && this.touchedView.css.minWidth) {
          if (newW < this.touchedView.css.minWidth.toPx()) {
            return;
          }
        }
        if (this.touchedView.rect && this.touchedView.rect.minWidth) {
          if (newW < this.touchedView.rect.minWidth) {
            return;
          }
        }
        const newH = this.startH + offsetY > 1 ? this.startH + offsetY : 1;
        css = {
          width: `${newW}px`,
        };
        if (type !== 'text') {
          if (type === 'image') {
            css.height = `${(newW * this.startH) / this.startW}px`;
          } else {
            css.height = `${newH}px`;
          }
        }
      } else {
        this.startX = x;
        this.startY = y;
        css = {
          left: `${rect.x + offsetX}px`,
          top: `${rect.y + offsetY}px`,
          right: undefined,
          bottom: undefined,
        };
      }
      this.doAction(
        {
          view: {
            css,
          },
        },
        null,
        !this.isScale,
      );
    },

    initScreenK() {
      if (!(getApp() && getApp().systemInfo && getApp().systemInfo.screenWidth)) {
        try {
          getApp().systemInfo = wx.getSystemInfoSync();
        } catch (e) {
          console.error(`Painter get system info failed, ${JSON.stringify(e)}`);
          return;
        }
      }
      this.screenK = 0.5;
      if (getApp() && getApp().systemInfo && getApp().systemInfo.screenWidth) {
        this.screenK = getApp().systemInfo.screenWidth / 750;
      }
      setStringPrototype(this.screenK, this.properties.scaleRatio);
    },

    initDancePalette() {
      if (this.properties.use2D) {
        return;
      }
      this.isDisabled = true;
      this.initScreenK();
      this.downloadImages(this.properties.dancePalette).then(async palette => {
        this.currentPalette = palette;
        const { width, height } = palette;

        if (!width || !height) {
          console.error(`You should set width and height correctly for painter, width: ${width}, height: ${height}`);
          return;
        }
        this.setData({
          painterStyle: `width:${width.toPx()}px;height:${height.toPx()}px;`,
        });
        this.frontContext || (this.frontContext = await this.getCanvasContext(this.properties.use2D, 'front'));
        this.bottomContext || (this.bottomContext = await this.getCanvasContext(this.properties.use2D, 'bottom'));
        this.topContext || (this.topContext = await this.getCanvasContext(this.properties.use2D, 'top'));
        this.globalContext || (this.globalContext = await this.getCanvasContext(this.properties.use2D, 'k-canvas'));
        new Pen(this.bottomContext, palette, this.properties.use2D).paint(() => {
          this.isDisabled = false;
          this.isDisabled = this.outterDisabled;
          this.triggerEvent('didShow');
        });
        this.globalContext.draw();
        this.frontContext.draw();
        this.topContext.draw();
      });
      this.touchedView = {};
    },

    startPaint() {
      this.initScreenK();
      const { width, height } = this.properties.palette;

      if (!width || !height) {
        console.error(`You should set width and height correctly for painter, width: ${width}, height: ${height}`);
        return;
      }

      let needScale = false;
      // 生成图片时,根据设置的像素值重新绘制
      if (width.toPx() !== this.canvasWidthInPx) {
        this.canvasWidthInPx = width.toPx();
        needScale = this.properties.use2D;
      }
      if (this.properties.widthPixels) {
        setStringPrototype(this.screenK, this.properties.widthPixels / this.canvasWidthInPx);
        this.canvasWidthInPx = this.properties.widthPixels;
      }

      if (this.canvasHeightInPx !== height.toPx()) {
        this.canvasHeightInPx = height.toPx();
        needScale = needScale || this.properties.use2D;
      }
      this.setData(
        {
          photoStyle: `width:${this.canvasWidthInPx}px;height:${this.canvasHeightInPx}px;`,
        },
        function () {
          this.downloadImages(this.properties.palette).then(async palette => {
            if (!this.photoContext) {
              this.photoContext = await this.getCanvasContext(this.properties.use2D, 'photo');
            }
            if (needScale) {
              const scale = getApp().systemInfo.pixelRatio;
              this.photoContext.width = this.canvasWidthInPx * scale;
              this.photoContext.height = this.canvasHeightInPx * scale;
              this.photoContext.scale(scale, scale);
            }
            new Pen(this.photoContext, palette).paint(() => {
              this.saveImgToLocal();
            });
            setStringPrototype(this.screenK, this.properties.scaleRatio);
          });
        },
      );
    },

    downloadImages(palette) {
      return new Promise((resolve, reject) => {
        let preCount = 0;
        let completeCount = 0;
        const paletteCopy = JSON.parse(JSON.stringify(palette));
        if (paletteCopy.background) {
          preCount++;
          downloader.download(paletteCopy.background, this.properties.LRU).then(
            path => {
              paletteCopy.background = path;
              completeCount++;
              if (preCount === completeCount) {
                resolve(paletteCopy);
              }
            },
            () => {
              completeCount++;
              if (preCount === completeCount) {
                resolve(paletteCopy);
              }
            },
          );
        }
        if (paletteCopy.views) {
          for (const view of paletteCopy.views) {
            if (view && view.type === 'image' && view.url) {
              preCount++;
              /* eslint-disable no-loop-func */
              downloader.download(view.url, this.properties.LRU).then(
                path => {
                  view.originUrl = view.url;
                  view.url = path;
                  wx.getImageInfo({
                    src: path,
                    success: res => {
                      // 获得一下图片信息,供后续裁减使用
                      view.sWidth = res.width;
                      view.sHeight = res.height;
                    },
                    fail: error => {
                      // 如果图片坏了,则直接置空,防止坑爹的 canvas 画崩溃了
                      console.warn(`getImageInfo ${view.originUrl} failed, ${JSON.stringify(error)}`);
                      view.url = '';
                    },
                    complete: () => {
                      completeCount++;
                      if (preCount === completeCount) {
                        resolve(paletteCopy);
                      }
                    },
                  });
                },
                () => {
                  completeCount++;
                  if (preCount === completeCount) {
                    resolve(paletteCopy);
                  }
                },
              );
            }
          }
        }
        if (preCount === 0) {
          resolve(paletteCopy);
        }
      });
    },

    saveImgToLocal() {
      const that = this;
      const optionsOf2d = {
        canvas: that.canvasNode,
      }
      const optionsOfOld = {
        canvasId: 'photo',
        destWidth: that.canvasWidthInPx,
        destHeight: that.canvasHeightInPx,
      }
      setTimeout(() => {
        wx.canvasToTempFilePath(
          {
            ...(that.properties.use2D ? optionsOf2d : optionsOfOld),
            success: function (res) {
              that.getImageInfo(res.tempFilePath);
            },
            fail: function (error) {
              console.error(`canvasToTempFilePath failed, ${JSON.stringify(error)}`);
              that.triggerEvent('imgErr', {
                error: error,
              });
            },
          },
          this,
        );
      }, 300);
    },

    getCanvasContext(use2D, id) {
      const that = this;
      return new Promise(resolve => {
        if (use2D) {
          const query = wx.createSelectorQuery().in(that);
          const selectId = `#${id}`;
          query
            .select(selectId)
            .fields({ node: true, size: true })
            .exec(res => {
              that.canvasNode = res[0].node;
              const ctx = that.canvasNode.getContext('2d');
              const wxCanvas = new WxCanvas('2d', ctx, id, true, that.canvasNode);
              resolve(wxCanvas);
            });
        } else {
          const temp = wx.createCanvasContext(id, that);
          resolve(new WxCanvas('mina', temp, id, true));
        }
      });
    },

    getImageInfo(filePath) {
      const that = this;
      wx.getImageInfo({
        src: filePath,
        success: infoRes => {
          if (that.paintCount > MAX_PAINT_COUNT) {
            const error = `The result is always fault, even we tried ${MAX_PAINT_COUNT} times`;
            console.error(error);
            that.triggerEvent('imgErr', {
              error: error,
            });
            return;
          }
          // 比例相符时才证明绘制成功,否则进行强制重绘制
          if (
            Math.abs(
              (infoRes.width * that.canvasHeightInPx - that.canvasWidthInPx * infoRes.height) /
                (infoRes.height * that.canvasHeightInPx),
            ) < 0.01
          ) {
            that.triggerEvent('imgOK', {
              path: filePath,
            });
          } else {
            that.startPaint();
          }
          that.paintCount++;
        },
        fail: error => {
          console.error(`getImageInfo failed, ${JSON.stringify(error)}`);
          that.triggerEvent('imgErr', {
            error: error,
          });
        },
      });
    },
  },
});

function setStringPrototype(screenK, scale) {
  /* eslint-disable no-extend-native */
  /**
   * string 到对应的 px
   * @param {Number} baseSize 当设置了 % 号时,设置的基准值
   */
  String.prototype.toPx = function toPx(_, baseSize) {
    if (this === '0') {
      return 0;
    }
    const REG = /-?[0-9]+(\.[0-9]+)?(rpx|px|%)/;

    const parsePx = origin => {
      const results = new RegExp(REG).exec(origin);
      if (!origin || !results) {
        console.error(`The size: ${origin} is illegal`);
        return 0;
      }
      const unit = results[2];
      const value = parseFloat(origin);

      let res = 0;
      if (unit === 'rpx') {
        res = Math.round(value * (screenK || 0.5) * (scale || 1));
      } else if (unit === 'px') {
        res = Math.round(value * (scale || 1));
      } else if (unit === '%') {
        res = Math.round((value * baseSize) / 100);
      }
      return res;
    };
    const formula = /^calc\((.+)\)$/.exec(this);
    if (formula && formula[1]) {
      // 进行 calc 计算
      const afterOne = formula[1].replace(/([^\s\(\+\-\*\/]+)\.(left|right|bottom|top|width|height)/g, word => {
        const [id, attr] = word.split('.');
        return penCache.viewRect[id][attr];
      });
      const afterTwo = afterOne.replace(new RegExp(REG, 'g'), parsePx);
      return calc(afterTwo);
    } else {
      return parsePx(this);
    }
  };
}

项目中引用:

<view class="shopView">
  <!-- 轮播 -->
  <view class="swp">
    <zswiper style="position: relative;" SwiperList="{{shopBase.slider_image_arr}}" current="{{current}}" bind:monitorCurrent="monitorCurrent"></zswiper>
    <view class="shopHead">
      <view><text>让利价:</text>¥{{shopBase.price}}</view>
      <view>市场价: ¥{{shopBase.ot_price}}</view>
    </view>
  </view>
  <!-- 介绍 -->
  <view class="shopRemark">
    <view class="shopTitle hiddenD">{{shopBase.title}}</view>
    <view class="shopbag">{{shopBase.keyword}}</view>
    <view class="shopyy">
      <view class="shopRadioView">剩余{{shopBase.stock}}{{shopBase.unit_name}} | 月销量{{shopBase.ficti }}{{shopBase.unit_name}}</view>
    </view>
    <view class="shopyys">
      <view class="colView">
        <image src="../../images/all/hd.png" mode="" /> <text>品质保障</text>
      </view>
      <view class="colView">
        <image src="../../images/all/twy.png" mode="" /> <text>退换无忧</text>
      </view>
      <view class="colView">
        <image src="../../images/all/che.png" mode="" /> <text>直采直销</text>
      </view>
    </view>

  </view>
  <!-- 优惠券 -->
<!-- 优惠券-->
<view wx:if="{{couponList.money}}">
<view class="yhq animated heartBeats delay-5s" bindtap="getSelected" data-id="{{couponList.id}}">
  <image src="../../images/all/yhq.png" wx:if="{{couponList.status==0}}" mode=""/>
  <image src="../../images/all/yyq.png" wx:else mode=""/>
  <view class="copunBox">
  <view>{{couponList.money}}元{{couponList.name}}</view>
  <view>部分商品通用</view>
  </view>
</view></view>
  <!-- end -->
  <!-- <view class="chListBox" wx:if="{{couponList.length>0}}" style="margin-bottom: 20rpx;" bindtap="onClose">
   <view class="cliflex">
    <label>优惠</label>
    <view class="cpubox">
    <view>满100减10</view>
    <view class="bgap"></view>
    <view>领券</view>
    </view>
   </view>
  <view class="flexicon">
    去使用
    <image src="../../images/all/right.png" mode=""/>
  </view>
  </view> -->
  <!-- 尺寸 -->
  <!-- <view class="chList" style="margin-bottom: 20rpx;">
    <label>包装尺寸</label>
    <text>100cm × 120cm × 60cm</text>
  </view> -->
  <view class="chList" style=" border-bottom-left-radius: 0;border-bottom-right-radius: 0;">
    <label>规格</label>
    <text>默认规格</text>
  </view>
  <view class="border"></view>
  <view class="chLists">
    <label>数量</label>
    <text>(当前剩余{{shopBase.stock }}件)</text>
    <view class="stepper">
      <van-stepper value="{{shopNum}}" bind:change="getShopNum" disable-input integer />
    </view>
  </view>
  <view class="chList" style="border-bottom-left-radius: 0;border-bottom-right-radius: 0;">
    <label>配送</label>
    <text>商家配送</text>
  </view>
  <view class="border"></view>
  <view class="chList" style="margin-bottom: 20rpx;">
    <label>服务</label>
    <text> 支持7天无理由退货</text>
  </view>
  <!-- 详情 -->
  <view class="shopBaseView">
    <view class="headT">
      <view class="text" style="font-weight: 600;">商品详情</view>
      <view class="druk"></view>
    </view>
    <richtext rich_content="{{shopBase.newHtml}}" />
  </view>
  <!-- 评价 -->
  <!-- <view class="shopBaseView">
    <view class="headT">
      <view class="text" style="font-weight: 600;">商品</view>
      <view class="druk"></view>
    </view>
  </view> -->
  <!-- 操作 -->
  <view class="submitView">
    <view class="iconviewtexts" bindtap="goHome">
      <button plain="true" class="iconviewtexts">
        <image src="../../images/all/sy.png" class="icons"></image>
        <text lines="1" class="texty">首页</text>
      </button>
    </view>
    <view>
      <view class="iconviewtexts" bindtap="initCollect" style="margin-left: 0rpx;" data-item="{{shopBase}}" wx:if="{{collectStatus==0}}" data-type="{{1}}">
        <image src="../../images/all/ai.png" class="icons"></image>
        <text lines="1" class="texty">收藏</text>
      </view>
      <view class="iconviewtexts" bindtap="initCollect" style="margin-left: 0rpx;" data-type="{{0}}" data-item="{{shopBase}}" wx:else>
        <image src="../../images/all/xss.png" class="icons animated rubberBand"></image>
        <text lines="1" class="texty">已收藏</text>
      </view>
    </view>
    <view>
      <view class="iconviewtexts" style="margin-left: 0;">
        <button plain="true" open-type="contact" class="iconviewtexts">
          <image src="../../images/all/tes.png" class="icons"></image>
          <text lines="1" class="texty">客服</text>
        </button>
      </view>
    </view>
    <view class="aleview">
      <view bindtap="getshare" class="rViewBtn">分享得{{shopBase.rebate_money}}元
        <view class="bubble animated animate__rubberBand">本商品每分享成功一次可得{{shopBase.rebate_money}}元</view>
      </view>
      <!-- <button plain="true" class="rViewBtn" open-type="share">分享得9元</button> -->
      <view class="rSelBtn" bindtap="getPayOrder" data-shopBase="{{shopBase}}">立即购买</view>
    </view>
  </view>
  <!-- end -->
</view>


<!-- 海报 -->
<van-overlay show="{{ show }}" z-index="999" bind:click="onClickHide">
  <view class="wrapper">
    <view style="height: 72rpx;"></view>
    <!-- poster -->
    <view class="block" catch:tap="noop">
      <view class="closeView" bindtap="getClose">
        <image src="../../images/all/cha.png" mode="" />
      </view>
      <view style="height: 30rpx;"></view>
      <view style='position: relative;'>
        <view class="top">
          <!-- <button bind:tap='onRevert'>撤销</button>
    <button bind:tap='onRecover'>恢复</button> -->
          <!-- <button bind:tap='saveImage'>保存</button> -->
        </view>
        <painter customActionStyle="{{customActionStyle}}" palette="{{paintPallette}}" bind:imgOK="onImgOK"  wx:if="{{show}}" action="{{action}}" widthPixels="580" />
        <image wx:if="{{show}}" src="{{image}}" style="width: 640rpx; height:928rpx;" />
      </view>

      <image class="logo" src="../../images/all/logo.png" mode="" />
    </view>
    <!-- end -->
    <!-- 底部弹窗 -->
    <view class="bottomView">
      <view class="headBoxs">
        <text>
          分享给好友,好友注册并下单后您可得{{shopBase.rebate_money}}元
        </text>
      </view>
      <view class="fwidth">
      <van-row gutter="20">
        <van-col class="colConter iconshars" span="12">
          <button open-type='share' plain="true">
            <image class="shareicons" src="../../images/all/s1.png" mode="" />
          </button>
        </van-col>
        <!-- <van-col class="colConter iconshars" span="8">
          <button open-type='share' plain="true">
            <image class="shareicons" src="../../images/all/s2.png" mode="" />
          </button>
        </van-col> -->
        <van-col class="colConter" span="12" bind:tap='saveImage'>
          <image class="shareicons" src="../../images/all/s3.png" mode="" />
        </van-col>
      </van-row>
    </view>
    <view class="fwidth">
      <van-row gutter="20">
        <van-col class="colConter" span="12">
          <button open-type='share' plain="true">微信好友</button>
        </van-col>
        <!-- <van-col class="colConter" span="8" bindtap="shopShare"> <button open-type='share' plain="true">朋友圈</button></van-col> -->
        <van-col class="colConter" span="12" bind:tap='saveImage'>保存图片</van-col>
      </van-row>
    </view>
    </view>
    <!-- end -->
  </view>

</van-overlay>

<!-- 优惠券 -->
<!-- 自定义图标 -->
<van-popup show="{{ modelSHow }}" closeable round z-index="999" position="bottom" custom-style="height: 50%;  background-color:#f5f5f5 ;" bind:close="onClose">
  <view class="headBox" bindtap="onClose">
    选择优惠券
  </view>
  <scroll-view class="couBox" scroll-y="true" bindscrolltoupper="upper" bindscrolltolower="lower" bindscroll="scroll" scroll-into-view="{{toView}}" scroll-top="{{scrollTop}}">
    <sCouList List="{{couponList}}" bind:getSelected="getSelected"></sCouList>
  </scroll-view>
  <view class="btnBox">
    <view class="btnview" bindtap="onClose">
      确定
    </view>
  </view>
</van-popup>

 

import {
  request
} from '../../utils/request'
Page({
  imagePath: '',
  history: [],
  future: [],
  isSave: false,
  /**
   * 页面的初始数据
   */
  data: {
    drawing: [],
    show: false,
    modelSHow: false,
    savebtnText: '点击按钮,保存图片',
    //轮播图的数组
    shopBase: {},
    shopNum: 1,
    shopid: 0,
    coupon_id:0,
    couponList: {},
  collectStatus: 0,
  // 海报生成
  paintPallette: {
    width: '640rpx',
    height: '928rpx',
    background: '#f5f5f5',
    left: 0,
    views: [{
        type: 'rect',
        css: {
          width: '580rpx',
          left: '30rpx',
          top: '30rpx',
          height: '766rpx',
          color: "#fff",
        },
      },
      {
        type: 'image',
        url: 'https://shop.rlmeijia.com/statics/uploads/recommendimg/recommendimg20221111141855.jpg',
        css: {
          top: '30rpx',
          left: '30rpx',
          width: '580rpx',
          height: '580rpx',
        },
      },
      {
        type: 'text',
        text: '【现货】民福康 天然维生素E软胶囊  100粒装',
        css: {
          top: '638rpx',
          left: '58rpx',
          width: "376rpx",
          lineHeight: "44rpx",
          color: '#333333',
          fontSize: "28rpx",

        },
      },
      {
        type: 'text',
        text: '¥194.00',
        css: {
          top: '730rpx',
          left: '58rpx',
          color: '#E1A765',
          fontSize: "32rpx",
        },
      },
      {
        type: 'qrcode',
        content: 'https://github.com/Kujiale-Mobile/Painter',
        css: {
          top: '638rpx',
          right: '62rpx',
          width: '96rpx',
          height: '96rpx',
        },
      },
      {
        type: 'text',
        text: '扫码购买',
        css: {
          top: '750rpx',
          right: '66rpx',
          color: '#333',
          fontSize: "22rpx",
        },
      },
      {
        type: 'image',
        url: '../../images/all/logo.png',
        css: {
          top: '818rpx',
          left: '212rpx',
          width: '222rpx',
          height: '72rpx',
        },
      },
    ],
  },
  customActionStyle: {
    border: {
      borderColor: '#1A7AF8',
    },
    scale: {
      textIcon: '/palette/switch.png',
      imageIcon: '/palette/scale.png',
    },
    delete: {
      icon: '/palette/close.png',
    },
  },
},
goHome() {
  wx.redirectTo({
    url: '/pages/index/index',
  })
},
getShopNum(e){
  console.error(e);
  this.setData({
    shopNum:e.detail
  })
},
/**
 * 生命周期函数--监听页面加载
 */
onLoad(options) {
   wx.showShareMenu({
    withShareTicket: true,
    menus: ['shareAppMessage', 'shareTimeline'],
    success(res){

    },fail(error){}
  })
  if (options) {

    this.initData(options.id)
    this.setData({
      shopid: options.id
    })
    this.initCouponList(options.id)
    this.getcollect(options.id)
    if (options.chnnel) {
      console.error("分享进入==>", options);
      wx.login({
        success: res => {
          // 发送 res.code 到后台换取 openId, sessionKey, unionId
          request("/login/getoauthtoken", 'post', {
            "authcode": res.code
          }).then(res => {
            if (res.code == 200) {
              console.error("粉丝添加获取传参==>", "商品id" + options.id, "老用户" + options.openid, "新用户" + res.openid);
              request("/login/addfriend", 'post', {
                pid: options.id,
                openid: options.openid,
                form_openid: res.openid,
                sitepage: 2
              }).then(res => {
                if (res.code == 0) {

                } else {

                }
              }).catch(err => {
                console.log(err);
              })
            } else {
              // wx.setStorageSync('openid', 1352168512)
            }


          }).catch(err => {
            console.log(err);
          })
        }
      })
    }
  }
},
/**
 * 获取优惠券列表
 * @param {*} options 
 */
initCouponList(id) {
  var openid = wx.getStorageSync('openid')
  request("/coupon/proccoupon", 'post', {
    openid: openid,
    pid: id
  }).then(res => {
    if (res.code == 0) {
      this.setData({
        couponList: res.data
      })
    } else {

    }
  }).catch(err => {
    console.log(err);
  })
},
/**
 * 初始化数据
 */
initData(id) {
  var that = this;
  request("/product/details/", 'post', {
    pid: id,
  }).then(res => {
    if (res.code == 0) {
      res.detailsRow.newHtml = res.detailsRow.detailed.replace(/(<img\s+.*?style=")(.*?)(".*?>)/g, function (match, p1, p2, p3) {
        var newStyle = 'width:100%;height:100%;';
        return p1 + newStyle + p3;
      });
      if (res.detailsRow.recommend_image != '') {
        console.error("判断是否有商品图片生成==>",res.detailsRow);
     var openid=wx.getStorageSync('openid')
    request("/login/createwxaqrcode", 'post', {
      path: "pages/shopBase/index?chnnel=1&id=" + this.data.shopid + "&openid=" + openid,
    }).then(data => {
      console.error("生成二维码==>",data);
      if (data.code == 0) {
        console.error("成功生成==>",data);
        that.setData({
          shopBase: res.detailsRow,
          paintPallette: {
            width: '640rpx',
            height: '928rpx',
            background: '#f5f5f5',
            left: 0,
            views: [{
                type: 'rect',
                css: {
                  width: '580rpx',
                  left: '30rpx',
                  top: '30rpx',
                  height: '766rpx',
                  color: "#fff",
                },
              },
              {
                type: 'image',
                url: res.detailsRow.recommend_image,
                css: {
                  top: '30rpx',
                  left: '30rpx',
                  width: '580rpx',
                  height: '580rpx',
                },
              },
              {
                type: 'text',
                text: res.detailsRow.title,
                css: {
                  top: '648rpx',
                  left: '58rpx',
                  width: "376rpx",
                  maxLines: 2,
                  lineHeight: "40rpx",
                  color: '#333333',
                  fontSize: "27rpx",

                },
              },
              {
                type: 'text',
                text: '¥' + res.detailsRow.price,
                css: {
                  top: '730rpx',
                  left: '58rpx',
                  color: '#E1A765',
                  fontSize: "32rpx",
                },
              },
              {
                type: 'image',
                url: data.wxaqrcodepath,
                css: {
                  top: '638rpx',
                  right: '62rpx',
                  width: '100rpx',
                  height: '100rpx',
                },
              },
              // {
              //   type: 'qrcode',
              //   content: data.wxaqrcodepath,
              //   css: {
              //     top: '638rpx',
              //     right: '62rpx',
              //     width: '96rpx',
              //     height: '96rpx',
              //   },
              // },
              {
                type: 'text',
                text: '扫码购买',
                css: {
                  top: '750rpx',
                  right: '66rpx',
                  color: '#333',
                  fontSize: "22rpx",
                },
              },
              {
                type: 'image',
                url: '../../images/all/logo.png',
                css: {
                  top: '818rpx',
                  left: '212rpx',
                  width: '222rpx',
                  height: '72rpx',
                },
              },
            ],
          }
        })
      } else {

      }
    }).catch(err => {
      console.log(err);
    })
     
      }
    } else {

    }
  }).catch(err => {
    console.log(err);
  })
},
/**
 * 生命周期函数--监听页面初次渲染完成
 */
onReady() {

},
/**
 * 商品是否收藏
 */
getcollect(id) {
  var openid = wx.getStorageSync('openid')
  request("/collect/getcollect", 'post', {
    openid: openid,
    cid: id
  }).then(res => {
    this.setData({
      collectStatus: res.code
    })

  }).catch(err => {
    console.log(err);
  })
},
getSelected(e) {
  var openid=wx.getStorageSync('openid')
request("/coupon/receivecoupon", 'post', {
  openid:openid,
  coupon_id:parseInt(e.currentTarget.dataset.id),
}).then(res => {
  if (res.code == 0) {
wx.showToast({
  title: '领取成功',
  icon:"none"
})
  } else {
    wx.showToast({
      title:res.msg,
      icon:"none"
    })
  }
}).catch(err => {
  console.log(err);
})
//   for (var i in this.data.couponList) {
//     if (e.detail.item.coupon_id == this.data.couponList[i].coupon_id) {
//       this.data.couponList[i].stats = 1;
// var openid=wx.getStorageSync('openid')
// request("/coupon/receivecoupon", 'post', {
//   openid:openid,
//   coupon_id:this.data.couponList[i].coupon_id,
// }).then(res => {
//   if (res.code == 0) {
// wx.showToast({
//   title: '领取成功',
//   icon:"none"
// })
//   } else {

//   }
// }).catch(err => {
//   console.log(err);
// })
//       this.setData({
//         coupon_id: this.data.couponList[i].coupon_id
//       })
//     } else {
//       this.data.couponList[i].stats = 0;
//     }
//   }
//   this.setData({
//     couponList: this.data.couponList
//   })
},
/**
 * 收藏
 */
initCollect(data, type) {
  var type = data.currentTarget.dataset.type;
  var item = data.currentTarget.dataset.item;
  var openid = wx.getStorageSync('openid')
  console.error("type", type);
  if (type == 1) {
    request("/collect/collect", 'post', {
      openid: openid,
      cid: item.id,
      type: 2,
      stats: type,
    }).then(res => {
      if (res.code == 0) {
        wx.showToast({
          title: '收藏成功',
          icon: "none",
        })
        this.setData({
          collectStatus: 1
        })
      }
    }).catch(err => {
      console.log(err);
    })
  } else {
    request("/collect/getcollect", 'post', {
      openid: openid,
      cid: item.id
    }).then(res => {
      console.error("“删除", res);
      if (res.code == 1) {
        wx.showToast({
          title: '取消收藏',
          icon: "none",
        })
        this.setData({
          collectStatus: 0
        })
      }
    }).catch(err => {
      console.log(err);
    })
  }
  return;

},
getClose() {
  this.setData({
    show: false
  })
},
onClose() {
  this.data.modelSHow = !this.data.modelSHow
  this.setData({
    modelSHow: this.data.modelSHow
  })
},
shopShare() {
  wx.shopShareMenu()
},
getshare() {
  var that = this;

  setTimeout(() => {
    if (this.data.shopBase.recommend_image != '') {
      that.setData({
        show: true,
      })
    } else {
      wx.showToast({
        title: '生成失败,请重新尝试',
      })
    }
  }, 800);



},
/**
 * 生命周期函数--监听页面显示
 */
onShow() {

},
getPayOrder(data) {
  var item = data.currentTarget.dataset.shopbase;
  var shopNum = this.data.shopNum;
  var arrbase = {};
  arrbase.id = item.id;
  arrbase.coupon_id=this.data.coupon_id;
  arrbase.shopNum = shopNum;
  arrbase.recommend_image = item.recommend_image;
  arrbase.title = item.title;
  arrbase.price = item.price;
  wx.setStorageSync('shopBase', arrbase)
  console.error(arrbase);
  wx.navigateTo({
    url: '/pages/nextOrder/index',
  })

},
/**
 * 生命周期函数--监听页面隐藏
 */
onHide() {},

/**
 * 生命周期函数--监听页面卸载
 */
onUnload() {

},

/**
 * 页面相关事件处理函数--监听用户下拉动作
 */
onPullDownRefresh() {

},

/**
 * 页面上拉触底事件的处理函数
 */
onReachBottom() {

},

/**
 * 用户点击右上角分享
 */
onShareAppMessage() {
 
  this.setData({
    show: false
  })
  var openid = wx.getStorageSync('openid')
  return {
    title: this.data.shopBase.title,
    path: '/pages/shopBase/index?chnnel=1&id=' + this.data.shopid + "&openid=" + openid,
  }

},
onShareTimeline(){
  this.setData({
    show:false
  })
  var openid = wx.getStorageSync('openid')
  return {
    title: this.data.shopBase.title,
    path: '/pages/shopBase/index?chnnel=1&id=' + this.data.shopid + "&openid=" + openid,
    imageUrl: "/images/share_3.png"
  }
},
// 海报生成
onImgOK(e) {
  this.imagePath = e.detail.path;

  this.setData({
    image: this.imagePath,
  });
  if (this.isSave) {
    this.saveImage(this.imagePath);
  }
},
saveImage() {
  var that=this;
  wx.showToast({
    title: '正在生成中,请稍后',
    icon:"none",
    duration:1500
  })
  var openid=wx.getStorageSync('openid')
  if (this.imagePath && typeof this.imagePath === 'string') {
    this.isSave = false;
    wx.getFileSystemManager().readFile({ // 读取本地文件内容
      filePath:this.imagePath,
      encoding: 'base64', //编码格式
      success(res) {
          request("/login/dowfile", 'post', {
      "fileUrl": res.data,
      openid:openid
    }, 3).then(gbase => {
console.error("https",gbase);
      var timestamp = Date.parse(new Date());  
    timestamp = timestamp / 1000;  
    console.error("传参",gbase.picUrl+"?timestamp="+timestamp);
      wx.downloadFile({
        url:gbase.picUrl+"?timestamp="+timestamp,
        success: res => {
          console.error("down-success保存到相册权限", res);
          //保存到相册
          wx.saveImageToPhotosAlbum({
            filePath: res.tempFilePath,
            success: res => {
              console.info("saveImageToPhotosAlbum-success===>", res);
              that.setData({
                show: false,
                // successShow: true
              })
              wx.showToast({
                title: '已保存到系统相册',
                icon:'none'
              })
            },
            fail: function (err) {
              console.info("saveImageToPhotosAlbum-fail===>", err);
                  wx.showModal({
                      title: '提示',
                      content: '需要您授权保存相册',
                      modalType: false,
                      success: modalSuccess => {
                          wx.openSetting({
                              success(settingdata) {
                                  console.log("settingdata", settingdata)
                                  if (settingdata.authSetting['scope.writePhotosAlbum']) {
                                      wx.showModal({
                                          title: '提示',
                                          content: '获取权限成功,再次点击图片即可保存',
                                          modalType: false,
                                      })
                                  } else {
                                      wx.showModal({
                                          title: '提示',
                                          content: '获取权限失败,将无法保存到相册哦~',
                                          modalType: false,
                                      })
                                  }
                              },
                              fail(failData) {
                                  console.log("failData", failData)
                              },
                              complete(finishData) {
                                  console.log("finishData", finishData)
                              }
                          })
                      }
                  })
          },complete(res) {
           that.setData({
             show:false
           })
        }

          })
        },
        fail: error => {
          console.error("downloadFile-fail失败2", error);
          wx.showToast({
            title: '保存失败,请稍后再试~',
            icon:'none'
          })
          that.setData({
            show: false,
          })
        },
      });
    
    }).catch(err => {
      console.log(err);
    })
      }
    })
  
 

    
    return;
    this.isSave = false;
    wx.saveImageToPhotosAlbum({
      filePath: this.imagePath,
    });
    
    console.error("保存",this.imagePath);
    this.setData({
      show: false
    })
    
  }
},


})
page {
  background: #F5F5F5;
  padding-bottom: 150rpx;
}

.shopRemark {
  width: 100%;
  margin: 0 auto;
  padding-bottom: 35rpx;
  background: #fff;
  border-bottom-left-radius: 20rpx;
  border-bottom-right-radius: 20rpx;
  /* margin-bottom: 20rpx; */
}
.abs{
  position: relative;
  left:  30rpx;
}
richtext img{
  width: 100!important;
}
.iconviewtexts  button[plain]{
  border: 0;
  border-radius: 0;
  position: relative;
  right: 15rpx;
}
.aleview button[plain]{
  border: 0;
  border-radius: 0;
  width: 208rpx;
  height: 98rpx;
  text-align: center;
  line-height: 98rpx;
  font-size: 28rpx;
  font-family: PingFangSC-Semibold, PingFang SC;
  font-weight: 600;
  color: #FFDB9E;
  background: linear-gradient(90deg, #505050 0%, #515151 100%);
}
.border {
  width: 666rpx;
  border-bottom: 1rpx solid #E4E4E4;
  margin: 0 auto;
}

.shopTitle {
  width: 686rpx;
  margin: 0 auto;
  font-size: 32rpx;
  padding-top: 20rpx;
  font-family: PingFangSC-Semibold, PingFang SC;
  font-weight: 600;
  color: #333333;
}

.shopbag {
  width: 686rpx;
  margin: 0 auto;
  font-size: 28rpx;
  font-family: PingFangSC-Regular, PingFang SC;
  font-weight: 400;
  color: #999999;
}

.shopyy {
  width: 686rpx;
  margin: 0 auto;
  margin-top: 16rpx;
}

.shopRadioView {
  width: 344rpx;
  display: flex;
  align-items: center;
  justify-content: center;
  height: 52rpx;
  padding: 0rpx 20rpx;

  background: #FFF3F3;
  font-size: 26rpx;
  font-family: PingFangSC-Medium, PingFang SC;
  font-weight: 500;
  color: #E1A765;
  border-radius: 28rpx;
}

.colView image {
  width: 24rpx;
  height: 24rpx;
  margin-right: 10rpx;
}

.shopyys {
  width: 686rpx;
  margin: 0 auto;
  margin-top: 16rpx;
  display: flex;
  align-items: center;
}

.colView {
  font-size: 24rpx;
  font-family: PingFangSC-Medium, PingFang SC;
  font-weight: 500;
  display: flex;
  align-items: center;
  color: #B38953;
}

.colView:nth-child(2) {
  margin-left: 80rpx;
}

.colView:nth-child(3) {
  margin-left: 80rpx;
}

.chList {
  width: 100%;
  height: 108rpx;
  margin: 0 auto;
  line-height: 108rpx;
  background: #FFFFFF;
  border-radius: 20rpx;
}
.chListBox{
  width: 100%;
  height: 108rpx;
  margin: 0 auto;
  display: flex;
  align-items: center;
  justify-content: space-between;
  line-height: 108rpx;
  background: #FFFFFF;
  border-radius: 20rpx;
}
.chListBox image{
  width: 16rpx;
  height: 32rpx;
  margin-right: 32rpx;
}
.chListBox label {
  margin-left: 20rpx;
  font-size: 28rpx;
  font-family: PingFangSC-Medium, PingFang SC;
  font-weight: 600;
  color: #333333;
}
.flexicon{
  display: flex;
  align-items: center;font-size: 26rpx;
  font-family: PingFangSC-Regular, PingFang SC;
  font-weight: 400;
  color: #666666;
}
.cpubox view:nth-child(1){
  margin-left: 12rpx;


}
.cpubox view:nth-child(2){
  width: 2rpx;
  height: 34rpx;
  border-right: 1rpx dotted #E1A765;

}
.cpubox view:nth-child(3){
  margin-right: 12rpx;

}
.cpubox{
  width: 228rpx;
height: 52rpx;
line-height: 52rpx;
position: relative;
margin-left: 86rpx;
text-align: center;
display: flex;
align-items: center;
justify-content: space-between;
border-radius: 10rpx;
font-size: 26rpx;
font-family: PingFangSC-Medium, PingFang SC;
font-weight: 500;
color: #E1A765;
background: #FFF3F3;
}
.cliflex{
  display: flex;
  align-items: center;
}
.flexicon image{
margin-left: 12rpx;
}
.chList label {
  margin-left: 32rpx;
  font-size: 28rpx;
  font-family: PingFangSC-Medium, PingFang SC;
  font-weight: 600;
  color: #333333;
}
.bu{
  width: 31rpx;
height: 30rpx;
background: #E1A765;
}
.chList text {
  font-size: 28rpx;
  font-family: PingFangSC-Medium, PingFang SC;
  font-weight: 500;
  color: #999999;
  margin-left: 30rpx;
}.chListBox text {
  font-size: 28rpx;
  font-family: PingFangSC-Medium, PingFang SC;
  font-weight: 500;
  color: #999999;
  margin-left: 30rpx;
}

.chLists {
  width: 100%;
  margin: 0 auto;
  height: 136rpx;
  border-bottom-right-radius: 20rpx;
  display: flex;
  border-bottom-left-radius: 20rpx;
  background-color: #fff;
  align-items: center;
  line-height: 136rpx;
  position: relative;
  margin-bottom: 20rpx;
}

.chLists label {
  font-size: 30rpx;
  font-family: PingFangSC-Medium, PingFang SC;
  font-weight: 600;
  color: #333333;
  margin-left: 32rpx;
  margin-right: 4rpx;
}

.chLists text {
  font-size: 26rpx;
  font-family: PingFangSC-Regular, PingFang SC;
  font-weight: 400;
  color: #666666;
}

.stepper {
  position: absolute;
  right: 38rpx;
}

.shopBaseView {
  width: 100%;
  margin: 0 auto;
  background-color: #fff;
  border-radius: 20rpx;
  padding-bottom: 30rpx;
}

.headT .text {
  padding-top: 30rpx;
  margin-left: 32rpx;
  font-size: 32rpx;
  font-family: PingFangSC-Medium, PingFang SC;
  font-weight: 500;
  height: 44rpx;
  color: #333333;
}

.druk {
  width: 32rpx;
  height: 8rpx;
  position: relative;
  top: 5rpx;
  left: 65rpx;
  border: 5rpx solid #E1A765;
  border-radius: 0 0 50% 50%/0 0 100% 100%;
  border-top: none;
  margin-bottom: 36rpx;
}

.shopBaseView image {
  margin: 0 auto;
  display: flex;
  justify-content: center;
}

.submitView {
  width: 100%;
  height: 98rpx;
  display: flex;
  align-items: center;
  background: #FFFFFF;
  position: fixed;
  justify-content: space-between;
  bottom: 0;
  z-index: 999;
}

.iconviewtexts {
  display: flex;
  width: 80rpx;
  text-align: center;
  align-items: center;
  /* background-color: red; */
  flex-direction: column;
  align-items: center;
  margin-left: 10rpx;
}


.shopHead view:nth-child(1){
  font-size: 48rpx;
font-family: PingFangSC-Semibold, PingFang SC;
font-weight: 600;
color: #E1A765;
display: flex;
align-items: center;

margin-left: 32rpx;
margin-bottom: 10rpx;
}
.shopHead view:nth-child(1) text{
  font-size: 30rpx;
font-family: PingFangSC-Semibold, PingFang SC;
font-weight: 600;
color: #E1A765;
}
.shopHead view:nth-child(2){
  font-size: 28rpx;
  font-family: PingFangSC-Regular, PingFang SC;
  font-weight: 400;margin-left: 32rpx;
  color: #999999;
  text-decoration: line-through;
}
.swp{
  position: relative;
}
.shopHead{
 padding-top: 20rpx; 
 width: 100%;
 margin: 0 auto;
 background: #fff;
 border-top-left-radius: 20rpx;
 border-top-right-radius: 20rpx;
 position: absolute;
 z-index: 8;
 bottom: 0rpx;
 text-align: left;
transform: translate(-50%);
left: 50%;
}
.texty {
  overflow-wrap: break-word;
  color: rgba(51,51,51,1);
  font-size: 22rpx;
  font-family: PingFangSC-Regular;
  font-weight: normal;
  text-align: right;
  white-space: nowrap;
  line-height: 20rpx;
  margin-top: 16rpx;
}

.icons {
  width: 36rpx;
  height: 32rpx;
  align-self: center;
}

.aleview {
  display: flex;
}

.rViewBtn {
  width: 208rpx;
  height: 98rpx;
  position: relative;
  text-align: center;
  line-height: 98rpx;
  font-size: 28rpx;
  font-family: PingFangSC-Semibold, PingFang SC;
  font-weight: 600;
  color: #FFDB9E;
  background: linear-gradient(90deg, #505050 0%, #515151 100%);
}

.rSelBtn {
  width: 250rpx;
  font-size: 28rpx;
  text-align: center;
  line-height: 98rpx;
  font-family: PingFangSC-Semibold, PingFang SC;
  font-weight: 600;
  color: #8A511A;
  height: 98rpx;
  background: linear-gradient(90deg, #E6C58E 0%, #E1A664 100%);
}

/* 海报 */
/* 点击分享弹窗 */
.wrapper {
  width: 100%;
  height: 2024rpx!important;
  overflow: auto;
}
.closeView{
  width: 48rpx;
  position: absolute;
height: 48rpx;
top: -25rpx;
right: -25rpx;
display: flex;
justify-content: center;
align-items: center;
border-radius: 50%;
z-index: 9;
background: rgba(0,0,0,0.5);
}
.overlays{
  width: 750rpx;
height: 1624rpx!important;
background: rgba(0,0,0,0.6);
overflow: auto!important;
}
.closeView image{
  width: 30rpx;
  height: 30rpx;
}
.block {
  width: 640rpx;
  position: relative;
  height: 928rpx;
  display: flex;
  justify-content: center;
  align-items: center;
  margin: 0 auto;
  background: #EEEEEE;
}


.canvas-style{
  width: 580rpx !important;
  height: 766rpx  !important;
  margin: 0 auto;
}
.savebtn-style{
  position: relative;
  top:220rpx!important;
  width: 130rpx!important;
  height: 162rpx!important;
  right: -265rpx!important;
  z-index: 999;
  /* opacity: 0; */
  /* display: none; */
}
.logo{
  display: flex;
  align-items: center;
  margin: 0 auto;
  width: 222rpx;
height: 72rpx;
margin-top: 22rpx;
}


.bottomView{
  width: 100%;
/* height: 376rpx; */
background: #FFFFFF;
position: fixed;
bottom: 0;

margin-top: 30rpx;
padding-bottom: 50rpx;
border: 2rpx solid #FFFFFF;
border-top-left-radius: 20rpx;
border-top-right-radius: 20rpx;
}
.headBoxs{
  width: 100%;
  border-top-left-radius: 20rpx;
border-top-right-radius: 20rpx;
  height: 110rpx;
  /* position: fixed;
  top: 0;
  z-index: 2; */
  text-align: center;
background: linear-gradient(180deg, rgba(230,142,142,0.83) 0%, rgba(225,166,100,0) 100%);
}
.headBoxs text{
  font-size: 30rpx;
  font-family: PingFangSC-Regular, PingFang SC;
  font-weight: 400;
  color: #8A511A;
}
.colConter{
  text-align: center;
    font-size: 28rpx;
  font-family: PingFangSC-Medium, PingFang SC;
  font-weight: 500;
  color: #333333;
  margin-top: 14rpx;
}
.shareicons{
  width: 100rpx;
height: 100rpx;
}
.colConter button[plain]{
  font-size: 28rpx;
font-family: PingFangSC-Medium, PingFang SC;
font-weight: 500;
color: #333333;
height: 40rpx;
line-height: 40rpx;
}
.iconshars button[plain]{
width: 100rpx;
height: 100rpx;
padding: 0;
margin: 0 auto;
}

/* 气泡 */
.bubble:before{
  position: absolute;
  top: 73rpx;
  left: 35%;
  content: '';
  border: 15rpx solid transparent;
  border-top-color: #302417;
  opacity: 0.9;
}
.bubble{
position: absolute;
width: 392rpx;
height: 73rpx;
top: -95rpx;
left: -50rpx;
text-align: center;
line-height: 73rpx;
font-size: 24rpx;
font-family: PingFangSC-Semibold, PingFang SC;
font-weight: 600;
color: #FFDB9E;
background: rgba(0,0,0,0.72);
border-radius: 30rpx;
box-shadow: 0rpx 4rpx 8rpx 0rpx rgba(0,0,0,0.9);
/* background-color: rgba(#302417,#302417,#302417, 0.8); */
/* background-color: rgb(#302417, #302417, #302417,0.5); */
/* border: 2rpx solid #302417; */
}

/* 优惠券 */
.headBox{
  width: 100%;
  text-align: center;
  display: flex;font-size: 36rpx;
  font-family: PingFang-SC-Bold, PingFang-SC;
  font-weight: bold;
  color: #333333;
  position: relative;
  align-items: center;
  height: 110rpx;
  justify-content: center;
  line-height: 110rpx;
  background: linear-gradient(180deg, rgba(230,142,142,0.83) 0%, rgba(225,166,100,0) 100%);
 }
 .headBox image{
   width: 40rpx;
   height: 40rpx;
 }
 .couBox{
   width: 100%;
   height: 320rpx;
   overflow: hidden;
 }
 .btnBox{
   width: 100%;
   position: fixed;
   bottom: 0;
height: 148rpx;
justify-content: center;
display: flex;
align-items: center;
background: #FFFFFF;
 }
.btnview{
  width: 686rpx;
  height: 84rpx;font-size: 32rpx;
  font-family: PingFangSC-Semibold, PingFang SC;
  font-weight: 600;
  color: #8A511A;
  line-height: 84rpx;
  text-align: center;
  background: linear-gradient(90deg, #E6C58E 0%, #E1A664 100%);
  box-shadow: 0rpx 4rpx 8rpx 0rpx rgba(255,59,60,0.25);
  border-radius: 42rpx;
 }


 /*  */

 .yhq{
  width: 702rpx;
  height: 218rpx;
  display: flex;
  align-items: center;
  margin: 0 auto;  position: relative;
}
.yhq image{
  position: relative;
  width: 702rpx;
  height: 218rpx;
  display: flex;
  align-items: center;
  margin: 0 auto;
}

.copunBox{
  position: absolute;
  z-index: 2;
  top: 60rpx;
  margin-left: 58rpx;
}
.copunBox view:nth-child(1){
  font-size: 40rpx;
font-family: PingFang-SC-Heavy, PingFang-SC;
font-weight: 800;
color: #EE5514;
}
.copunBox view:nth-child(2){
  font-size: 22rpx;
  font-family: PingFangSC-Medium, PingFang SC;
  font-weight: 500;
  color: #EE5514;
}

.fwidth{
  width: 80%;
  margin: 0 auto;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/742556.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

揭开液体活检技术的神秘面纱

液体活检&#xff08;liquid biopsy&#xff09;是新兴的肿瘤诊断技术&#xff0c;与传统检测手段相比具有创伤性小、取样便捷、可实时动态检测等优势&#xff0c;在肿瘤早期筛查、分子分型、复发监测和预后评估等方面起到重要作用。 图 1 与传统的组织活检相比&#xff0c;液…

路径规划算法:基于爬行动物优化的路径规划算法- 附代码

路径规划算法&#xff1a;基于爬行动物优化的路径规划算法- 附代码 文章目录 路径规划算法&#xff1a;基于爬行动物优化的路径规划算法- 附代码1.算法原理1.1 环境设定1.2 约束条件1.3 适应度函数 2.算法结果3.MATLAB代码4.参考文献 摘要&#xff1a;本文主要介绍利用智能优化…

【分布式应用】Filebeat+ELK 部署、logstash filter四大过滤插件

目录 一、 FilebeatELK 部署1.1在 Filebeat 节点上操作1.1.1安装 Filebeat1.1.2设置 filebeat 的主配置文件1.1.3 在 Logstash 组件所在节点上新建一个 Logstash 配置文件1.1.4浏览器访问测试 二 、filter四大插件2.1grok 正则捕获插件2.1.1内置正则表达式调用 2.2logstash 官方…

操作系统——输入输出IO管理

文章目录 **1 I/O管理概述****1.1 I/O设备****1.2 I/O控制方式****1.2.1 程序直接控制方式****1.2.2 中断驱动方式****1.2.3 DMA方式****1.2.4 通道控制方式** **1.3 I/O软件层次结构****1.4 应用程序I/O接口** **2 设备独立性软件****2.1 高速缓冲与缓冲区****2.2 设备分配和回…

车牌识别--http协议

文章目录 libcurl车牌识别 前言 基于https协议访问网址实现车牌识别 http是超文本传输协议&#xff0c;它可以在B/S模式下解决tcp传输格式的问题&#xff0c;处于tcp之上在应用层 https是http加密版本&#xff0c;通过httpssl实现加密版http 我们车牌识别通过http协议调用翔云…

Django_admin数据管理后台

目录 一、基础操作 二、自定义后台操作数据行为 源码等资料获取方法 admin数据管理后台是django内置的一个后台管理界面&#xff0c;能查看已注册模型类的数据结构&#xff0c;以及对数据的增删改。 一、基础操作 1.1 检查项目目录下的urls.py有没有如下配置 1.2 创建djan…

使用U盘在无系统的机器上安装Ubuntu

1、在Bios中选择将USB 作为Boot Option #1 2、插入制作好的系统盘&#xff08;系统盘制作可以看上一篇博客&#xff09; 3、save & exit选择 save change and reset 4、 开始安装系统 如果出现安装窗口过大无法点击下一步&#xff0c;可以使用ALTF7拖动窗口。

java中的BIO NIO AIO

多路复用IO模型是目前使用的比较多的模型。java中的NIO常用的理解是在 网络IO中&#xff0c;那么在网络IO中为什么NIO比BIO效率更高&#xff1f;我们的web项目中是用的哪种呢&#xff1f;可以往下看。 JavaNIO实际上就是多路复用IO。在多路复用IO模型中&#xff0c;会有一个线程…

【Unity】Unity2022设置2D动画的sample

首先在主页面选择Animation 出现这样的一个框 点击红点行&#xff0c;最右侧的三个点 将Show Sample Rate勾选上即可

Fiddler工具 — Fiddler常用插件(Willow)

Fiddler已有的功能已经够我们日常工作中使用了&#xff0c;为了更好的扩展Fiddler&#xff0c;Fiddler也是支持一些插件的安装&#xff0c;也支持用户自己开发插件并安装。 Fiddler插件下载地址&#xff1a;https://www.telerik.com/fiddler/add-ons 1、Traific Difer插件 Tr…

【Java】Map(包括HashMap)

Map HashMap 和 Hashtable 的区别HashMap 和 HashSet 区别HashMap 的底层实现JDK1.8 之前JDK1.8红黑树 HashMap 的长度为什么是 2 的幂次方HashMap 多线程操作导致死循环问题HashMap 为什么线程不安全&#xff1f; ConcurrentHashMapConcurrentHashMap 和 Hashtable 的区别JDK …

常见面试题之JVM实践(调优)

1. JVM调优的参数可以在哪里设置参数值&#xff1f; 1.1 tomcat的设置vm参数 修改TOMCAT_HOME/bin/catalina.sh文件&#xff0c;如下图&#xff1a; JAVA_OPTS"-Xms512m -Xmx1024m" 1.2 springboot项目jar文件启动 通常在linux系统下直接加参数启动springboot项…

在Pandas中处理缺失数据

当没有为一个或多个项目或整个单元提供信息时&#xff0c;可能会出现数据缺失。缺失数据在现实生活中是一个非常大的问题。缺失数据在pandas中也可以称为NA&#xff08;不可用&#xff09;值。在DataFrame中&#xff0c;有时许多数据集只是缺少数据&#xff0c;因为它存在而未被…

52 # 二叉树的前中后遍历

二叉树的遍历 线性数据结构遍历比较简单&#xff0c;可以采用正序遍历、逆序遍历。 遍历树的目的一般是修改树&#xff0c;比如修改树的节点&#xff0c;采用访问者模式 前序遍历 前序遍历&#xff08;preorder traversal&#xff09;&#xff1a;先访问根节点&#xff0c;…

Go语言struct要使用 tags的原因解析

这篇文章主要介绍了为什么 Go 语言 struct 要使用 tags,在本文中&#xff0c;我们将探讨为什么 Go 语言中需要使用 struct tags&#xff0c;以及 struct tags 的使用场景和优势&#xff0c;需要的朋友可以参考下 在 Go 语言中&#xff0c;struct 是一种常见的数据类型&#xf…

由于找不到vcomp100.dll,无法继续执行代码,解决方法

为什么会由于找不到vcomp100.dll,无法继续执行代码问题呢&#xff1f; 文件被误删除&#xff1a;有时候&#xff0c;在进行系统清理或卸载应用程序时&#xff0c;可能会不小心删除了vcomp100.dll文件。如果某个程序依赖于该文件&#xff0c;并且文件被删除&#xff0c;那么该程…

ESP32开发:IDFV4.4配置LVGL8.3

配置LVGL8.3源码 LVGL GITHUB代码仓库如下&#xff1a;https://github.com/lvgl/lvgl/tree/release/v8.3 官方已经在ESP32上移植好的代码demo&#xff0c;目前最新版是LVGL 7.9&#xff1a;https://github.com/lvgl/lv_port_esp32 我们可以将LVGL官方配置好的ESP32 LVGL仓库下…

超详细的学习笔记:CSS盒子模型(附代码示例)

目录 一、CSS三大特性 1、继承性 2、层叠性 3、优先级 4、权重叠加的计算 二、PxCook的基本使用 三、盒子模型 1、盒子模型的介绍 2、内容的宽度和高度 3、边框 (border) 1、连写形式 2、单方向设置 3、单个属性 8、内边距&#xff08;padding&#xff09;和外边…

arm学习stm32之spi总线数码管倒计时

由于时间没有用时间计时器操作&#xff0c;有些误差&#xff0c;后续有空会翻新计时器版本 main.c #include "spi.h" extern void printf(const char *fmt, ...); void delay_ms(int ms) {int i,j;for(i 0; i < ms;i)for (j 0; j < 1800; j); } int num[10…

钉钉提示 redirect_url的域名不在appid的安全域名内

钉钉提示 redirect_url的域名不在appid的安全域名内 1、需要在《钉钉开放平台》- 开发者后台设置《钉钉扫码登陆功能》 2、如果钉钉界面没有钉钉扫码登陆功能-》点击浏览器右下角-》《返回旧版》 3、备注&#xff1a;当前访问的IP地址跟钉钉扫码登陆功能填写的IP地址需保持一致…