uniapp实现抽奖功能

2023-11-15

效果

在这里插入图片描述

  • 代码
<template>
  <view class="almost-lottery">
    <view class="almost-lottery__wrap" :style="{ width: lotterySize + 'rpx', height: lotterySize + 'rpx' }">
      <view class="lottery-action" :style="{ width: actionSize + 'rpx', height: actionSize + 'rpx', left: canvasMarginOutside + 'rpx' }"></view>
      <view class="str-margin-outside" :style="{ left: strMarginOutside + 'rpx' }"></view>
      <view class="img-margin-str" :style="{ left: imgMarginStr + 'rpx' }"></view>
      <view class="img-size" :style="{ width: imgWidth + 'rpx', height: imgHeight + 'rpx' }"></view>
      <template v-if="lotteryImg">
        <image
          class="almost-lottery__bg"
          mode="widthFix"
          :src="lotteryBg"
          :style="{
            width: lotteryPxSize + 'px',
            height: lotteryPxSize + 'px'
          }"
        ></image>
        <image
          class="almost-lottery__canvas-img"
          mode="widthFix"
          :src="lotteryImg"
          :style="{
            width: canvasPxSize + 'px',
            height: canvasPxSize  + 'px',
            transform: `rotate(${canvasAngle + targetAngle}deg)`,
            transitionDuration: `${transitionDuration}s`
          }"
        ></image>
        <image
          class="almost-lottery__action almost-lottery__action-bg"
          mode="widthFix"
          :src="actionBg"
          :style="{
            width: actionPxSize + 'px',
            height: actionPxSize + 'px',
            transform: `rotate(${actionAngle + targetActionAngle}deg)`,
            transitionDuration: `${transitionDuration}s`
          }"
          @click="handleActionStart"
        ></image>
      </template>
    
      <!-- 正在绘制转盘时的提示文本 -->
      <text class="almost-lottery__tip" v-else>{{ almostLotteryTip }}</text>
    </view>
    
    <!-- 为了兼容 app 端 ctx.measureText 所需的标签 -->
    <text class="almost-lottery__measureText" :style="{ fontSize: higtFontSize + 'px' }">{{ measureText }}</text>
    
    <!-- #ifdef MP-ALIPAY -->
    <canvas 
      :class="className"
      :id="canvasId"
      :width="higtCanvasSize"
      :height="higtCanvasSize"
      :style="{
        width: higtCanvasSize + 'px',
        height: higtCanvasSize + 'px'
      }"
    />
    <!-- #endif -->
    <!-- #ifndef MP-ALIPAY -->
    <canvas
      :class="className"
      :canvas-id="canvasId"
      :width="higtCanvasSize"
      :height="higtCanvasSize"
      :style="{
        width: higtCanvasSize + 'px',
        height: higtCanvasSize + 'px'
      }"
    />
    <!-- #endif -->
  </view>
</template>
  • js代码
 const systemInfo = uni.getSystemInfoSync()
	import { getStore, setStore, clearStore, clacTextLen, downloadFile, pathToBase64, base64ToPath } from '@/uni_modules/almost-lottery/utils/almost-utils.js'
  export default {
    name: 'AlmostLottery',
    props: {
      // 设计稿的像素比基准值
      pixelRatio: {
        type: Number,
        default: 2
      },
      // canvas 标识
      canvasId: {
        type: String,
        default: 'almostLottery'
      },
      // 抽奖转盘的整体尺寸
      lotterySize: {
        type: Number,
        default: 600
      },
      // 抽奖按钮的尺寸
      actionSize: {
        type: Number,
        default: 200
      },
			// canvas边缘距离转盘边缘的距离
			canvasMarginOutside: {
        type: Number,
        default: 90
      },
      // 奖品列表
      prizeList: {
        type: Array,
        required: true,
        validator: (value) => {
          return value.length > 1
        }
      },
      // 中奖奖品在列表中的下标
      prizeIndex: {
        type: Number,
        required: true
      },
      // 奖品区块对应背景颜色
      colors: {
        type: Array,
        default: () => [
          '#FFFFFF',
          '#FFBF05'
        ]
      },
      // 转盘外环背景图
      lotteryBg: {
        type: String,
        default: '/uni_modules/almost-lottery/static/almost-lottery/almost-lottery__bg2x.png'
      },
      // 抽奖按钮背景图
      actionBg: {
        type: String,
        default: '/uni_modules/almost-lottery/static/almost-lottery/almost-lottery__action2x.png'
      },
      // 是否绘制奖品名称
      prizeNameDrawed: {
				type: Boolean,
				default: true
      },
      // 是否开启奖品区块描边
      stroked: {
				type: Boolean,
				default: false
			},
      // 描边颜色
      strokeColor: {
        type: String,
        default: '#FFBF05'
      },
      // 旋转的类型
      rotateType: {
        type: String,
        default: 'roulette'
      },
      // 旋转动画时间 单位s
      duration: {
        type: Number,
        default: 8
      },
      // 旋转的圈数
      ringCount: {
        type: Number,
        default: 8
      },
      // 指针位置
      pointerPosition: {
        type: String,
        default: 'edge',
        validator: (value) => {
          return value === 'edge' || value === 'middle'
        }
      },
      // 文字方向
      strDirection: {
        type: String,
        default: 'horizontal',
        validator: (value) => {
          return value === 'horizontal' || value === 'vertical'
        }
      },
      // 字体颜色
      strFontColors: {
        type: Array,
        default: () => [
          '#FFBF05',
          '#FFFFFF'
        ]
      },
      // 文字的大小
      strFontSize: {
        type: Number,
        default: 24
      },
      // 奖品文字距离边缘的距离
      strMarginOutside: {
        type: Number,
        default: 0
      },
      // 奖品图片距离奖品文字的距离
      imgMarginStr: {
        type: Number,
        default: 60
      },
      // 奖品文字多行情况下的行高
      strLineHeight: {
        type: Number,
        default: 1.2
      },
      // 奖品文字总长度限制
      strMaxLen: {
        type: Number,
        default: 12
      },
      // 奖品文字多行情况下第一行文字长度
      strLineLen: {
        type: Number,
        default: 6
      },
      // 奖品图片的宽
      imgWidth: {
        type: Number,
        default: 50
      },
      // 奖品图片的高
      imgHeight: {
        type: Number,
        default: 50
      },
			// 是否绘制奖品图片
			imgDrawed: {
				type: Boolean,
				default: true
			},
			// 转盘绘制成功的提示
			successMsg: {
				type: String,
				default: '奖品准备就绪,快来参与抽奖吧'
			},
			// 转盘绘制失败的提示
			failMsg: {
				type: String,
				default: '奖品仍在准备中,请稍后再来...'
			},
			// 是否开启画板的缓存
			canvasCached: {
				type: Boolean,
				default: false
			}
    },
    data() {
      return {
        // 画板className
        className: 'almost-lottery__canvas',
        // 抽奖转盘的整体px尺寸
        lotteryPxSize: 0,
        // 画板的px尺寸
        canvasPxSize: 0,
        // 抽奖按钮的px尺寸
        actionPxSize: 0,
        // 奖品文字距离转盘边缘的距离
        strMarginPxOutside: 0,
        // 奖品图片相对奖品文字的距离
        imgMarginPxStr: 0,
        // 奖品图片的宽、高
        imgPxWidth: 0,
        imgPxHeight: 0,
        // 画板导出的图片
        lotteryImg: '',
        // 旋转到奖品目标需要的角度
        targetAngle: 0,
        targetActionAngle: 0,
        // 旋转动画时间 单位 s
        transitionDuration: 0,
        // 是否正在旋转
        isRotate: false,
        // 当前停留在那个奖品的序号
        stayIndex: 0,
        // 当前中奖奖品的序号
        targetIndex: 0,
				// 是否存在可用的缓存转盘图
				isCacheImg: false,
				oldLotteryImg: '',
        // 转盘绘制时的提示
        almostLotteryTip: '奖品准备中...',
        // 解决 app 不支持 measureText 的问题
				// app 已在 2.9.3 的版本中提供了对 measureText 的支持,将在后续版本逐渐稳定后移除相关兼容代码
        measureText: ''
      }
    },
    computed: {
      // 高清尺寸
      higtCanvasSize() {
        return this.canvasPxSize * systemInfo.pixelRatio
      },
      // 高清字体
      higtFontSize() {
        return (this.strFontSize / this.pixelRatio) * systemInfo.pixelRatio
      },
      // 高清行高
      higtHeightMultiple() {
        return (this.strFontSize / this.pixelRatio) * this.strLineHeight * systemInfo.pixelRatio
      },
      // 根据奖品列表计算 canvas 旋转角度
      canvasAngle() {
        let result = 0
        
        let prizeCount = this.prizeList.length
        let prizeClip = 360 / prizeCount
        let diffNum = 90 / prizeClip
        if (this.pointerPosition === 'edge' || this.rotateType === 'pointer') {
          result = -(prizeClip * diffNum)
        } else {
          result = -(prizeClip * diffNum + prizeClip / 2)
        }
        return result
      },
      actionAngle() {
        return 0
      },
      // 外圆的半径
      outsideRadius() {
        return this.higtCanvasSize / 2
      },
      // 内圆的半径
      insideRadius() {
        return 20 * systemInfo.pixelRatio
      },
      // 文字距离边缘的距离
      textRadius() {
        return this.strMarginPxOutside * systemInfo.pixelRatio || (this.higtFontSize / 2)
      },
      // 根据画板的宽度计算奖品文字与中心点的距离
      textDistance() {
        const textZeroY = Math.round(this.outsideRadius - (this.insideRadius / 2))
        return textZeroY - this.textRadius
      }
    },
    watch: {
      // 监听获奖序号的变动
      prizeIndex(newVal, oldVal) {
        if (newVal > -1) {
          this.targetIndex = newVal
          this.onRotateStart()
        } else {
          console.info('旋转结束,prizeIndex 已重置')
        }
      }
    },
    methods: {
      // 开始旋转
      onRotateStart() {
        if (this.isRotate) return
        this.isRotate = true
        // 奖品总数
        let prizeCount = this.prizeList.length
        let baseAngle = 360 / prizeCount
        let angles = 0
        
        if (this.rotateType === 'pointer') {
          if (this.targetActionAngle === 0) {
            // 第一次旋转
            angles = (this.targetIndex - this.stayIndex) * baseAngle + baseAngle / 2 - this.actionAngle
          } else {
            // 后续旋转
            // 后续继续旋转 就只需要计算停留的位置与目标位置的角度
            angles = (this.targetIndex - this.stayIndex) * baseAngle
          }
          
          // 更新目前序号
          this.stayIndex = this.targetIndex
          // 转 8 圈,圈数越多,转的越快
          this.targetActionAngle += angles + 360 * this.ringCount
          console.log('targetActionAngle', this.targetActionAngle)
        } else {
          if (this.targetAngle === 0) {
            // 第一次旋转
            // 因为第一个奖品是从0°开始的,即水平向右方向
            // 第一次旋转角度 = 270度 - (停留的序号-目标序号) * 每个奖品区间角度 - 每个奖品区间角度的一半 - canvas自身旋转的度数
            angles = (270 - (this.targetIndex - this.stayIndex) * baseAngle - baseAngle / 2) - this.canvasAngle
          } else {
            // 后续旋转
            // 后续继续旋转 就只需要计算停留的位置与目标位置的角度
            angles = -(this.targetIndex - this.stayIndex) * baseAngle
          }
          
          // 更新目前序号
          this.stayIndex = this.targetIndex
          // 转 8 圈,圈数越多,转的越快
          this.targetAngle += angles + 360 * this.ringCount
        }

        // 计算转盘结束的时间,预加一些延迟确保转盘停止后触发结束事件
        let endTime = this.transitionDuration * 1000 + 100
        let endTimer = setTimeout(() => {
          clearTimeout(endTimer)
          endTimer = null

          this.isRotate = false
          this.$emit('draw-end')
        }, endTime)

        let resetPrizeTimer = setTimeout(() => {
          clearTimeout(resetPrizeTimer)
          resetPrizeTimer = null

          // 每次抽奖结束后都要重置父级组件的 prizeIndex
          this.$emit('reset-index')
        }, endTime + 50)
      },
      // 点击 开始抽奖 按钮
      handleActionStart() {
        if (!this.lotteryImg) return
        if (this.isRotate) return
        this.$emit('draw-start')
      },
      // 渲染转盘
      async onCreateCanvas() {
        // 获取 canvas 画布
        const canvasId = this.canvasId
        const ctx = uni.createCanvasContext(canvasId, this)

        // canvas 的宽高
        let canvasW = this.higtCanvasSize
        let canvasH = this.higtCanvasSize

        // 根据奖品个数计算 角度
        let prizeCount = this.prizeList.length
        let baseAngle = Math.PI * 2 / prizeCount

        // 设置字体
        ctx.setFontSize(this.higtFontSize)

        // 注意,开始画的位置是从0°角的位置开始画的。也就是水平向右的方向。
        // 画具体内容
        for (let i = 0; i < prizeCount; i++) {
					let prizeItem = this.prizeList[i]
          // 当前角度
          let angle = i * baseAngle

          // 保存当前画布的状态
          ctx.save()
          
          // x => 圆弧对应的圆心横坐标 x
          // y => 圆弧对应的圆心横坐标 y
          // radius => 圆弧的半径大小
          // startAngle => 圆弧开始的角度,单位是弧度
          // endAngle => 圆弧结束的角度,单位是弧度
          // anticlockwise(可选) => 绘制方向,true 为逆时针,false 为顺时针
          
          ctx.beginPath()
          // 外圆
          ctx.arc(canvasW * 0.5, canvasH * 0.5, this.outsideRadius, angle, angle + baseAngle, false)
          // 内圆
          ctx.arc(canvasW * 0.5, canvasH * 0.5, this.insideRadius, angle + baseAngle, angle, true)
          
          // 每个奖品区块背景填充颜色
          if (this.colors.length === 2) {
            ctx.setFillStyle(this.colors[i % 2])
          } else {
            ctx.setFillStyle(this.colors[i])
          }
          // 填充颜色
          ctx.fill()
          
          // 设置文字颜色
          if (this.strFontColors.length === 1) {
            ctx.setFillStyle(this.strFontColors[0])
          } else if (this.strFontColors.length === 2) {
            ctx.setFillStyle(this.strFontColors[i % 2])
          } else {
            ctx.setFillStyle(this.strFontColors[i])
          }
          
          // 开启描边
          if (this.stroked) {
            // 设置描边颜色
            ctx.setStrokeStyle(`${this.strokeColor}`)
            // 描边
            ctx.stroke()
          }

          // 开始绘制奖品内容
          // 重新映射画布上的 (0,0) 位置
          let translateX = canvasW * 0.5 + Math.cos(angle + baseAngle / 2) * this.textDistance
          let translateY = canvasH * 0.5 + Math.sin(angle + baseAngle / 2) * this.textDistance
          ctx.translate(translateX, translateY)

          // 绘制奖品名称
          let rewardName = this.strLimit(prizeItem.prizeName)
          
          // rotate方法旋转当前的绘图,因为文字是和当前扇形中心线垂直的
          ctx.rotate(angle + (baseAngle / 2) + (Math.PI / 2))

          // 设置文本位置并处理换行
          if (this.strDirection === 'horizontal') {
            // 是否需要换行
            if (rewardName && this.prizeNameDrawed) {
              let realLen = clacTextLen(rewardName).realLen
              let isLineBreak = realLen > this.strLineLen
              if (isLineBreak) {
                // 获得多行文本数组
                let firstText = ''
                let lastText = ''
                let firstCount = 0
                for (let j = 0; j < rewardName.length; j++) {
                  firstCount += clacTextLen(rewardName[j]).byteLen
                  if (firstCount <= (this.strLineLen * 2)) {
                    firstText += rewardName[j]
                  } else {
                    lastText += rewardName[j]
                  }
                }
                rewardName = firstText + ',' + lastText
                let rewardNames = rewardName.split(',')
                // 循环文本数组,计算每一行的文本宽度
                for (let j = 0; j < rewardNames.length; j++) {
                  if (ctx.measureText && ctx.measureText(rewardNames[j]).width > 0) {
                    // 文本的宽度信息
                    let tempStrSize = ctx.measureText(rewardNames[j])
                    let tempStrWidth = -(tempStrSize.width / 2).toFixed(2)
                    ctx.fillText(rewardNames[j], tempStrWidth, j * this.higtHeightMultiple)
                  } else {
                    this.measureText = rewardNames[j]
                    
                    // 等待页面重新渲染
                    await this.$nextTick()
                    
                    let textWidth = await this.getTextWidth()
                    let tempStrWidth = -(textWidth / 2).toFixed(2)
                    ctx.fillText(rewardNames[j], tempStrWidth, j * this.higtHeightMultiple)
                    // console.log(rewardNames[j], textWidth, j)
                  }
                }
              } else {
                if (ctx.measureText && ctx.measureText(rewardName).width > 0) {
                  // 文本的宽度信息
                  let tempStrSize = ctx.measureText(rewardName)
                  let tempStrWidth = -(tempStrSize.width / 2).toFixed(2)
                  ctx.fillText(rewardName, tempStrWidth, 0)
                } else {
                  this.measureText = rewardName
                  
                  // 等待页面重新渲染
                  await this.$nextTick()
                  
                  let textWidth = await this.getTextWidth()
                  let tempStrWidth = -(textWidth / 2).toFixed(2)
                  ctx.fillText(rewardName, tempStrWidth, 0)
                }
              }
            }
          } else {
            let rewardNames = rewardName.split('')
            for (let j = 0; j < rewardNames.length; j++) {
              if (ctx.measureText && ctx.measureText(rewardNames[j]).width > 0) {
                // 文本的宽度信息
                let tempStrSize = ctx.measureText(rewardNames[j])
                let tempStrWidth = -(tempStrSize.width / 2).toFixed(2)
                ctx.fillText(rewardNames[j], tempStrWidth, j * this.higtHeightMultiple)
              } else {
                this.measureText = rewardNames[j]
                
                // 等待页面重新渲染
                await this.$nextTick()
                
                let textWidth = await this.getTextWidth()
                let tempStrWidth = -(textWidth / 2).toFixed(2)
                ctx.fillText(rewardNames[j], tempStrWidth, j * this.higtHeightMultiple)
                // console.log(rewardNames[j], textWidth, i)
              }
            }
          }
          

          // 绘制奖品图片,文字竖向展示时,不支持图片展示
          if (this.imgDrawed && prizeItem.prizeImage && this.strDirection !== 'vertical') {
						// App-Android平台 系统 webview 更新到 Chrome84+ 后 canvas 组件绘制本地图像 uni.canvasToTempFilePath 会报错
						// 统一将图片处理成 base64
						// https://ask.dcloud.net.cn/question/103303
						let reg = /^(https|http)/g
						// 处理远程图片
						if (reg.test(prizeItem.prizeImage)) {
              let platformTips = ''
              // #ifdef APP-PLUS
							platformTips = ''
              // #endif
              // #ifdef MP
							platformTips = '需要处理好下载域名的白名单问题,'
              // #endif
              // #ifdef H5
							platformTips = '需要处理好跨域问题,'
              // #endif
							console.warn(`###当前数据列表中的奖品图片为网络图片,${platformTips}开始尝试下载图片...###`)
							let res = await downloadFile(prizeItem.prizeImage)
							console.log('处理远程图片', res)
							if (res.ok) {
								let tempFilePath = res.tempFilePath
								// #ifndef MP
								prizeItem.prizeImage = await pathToBase64(tempFilePath)
								// #endif
								// #ifdef MP
								prizeItem.prizeImage = tempFilePath
								// #endif
							} else {
                this.handlePrizeImgSuc({
                  ok: false,
                  data: res.data,
                  msg: res.msg
                })
              }
						} else {
							// #ifndef MP
              // 不是小程序环境,把本地图片处理成 base64
              if (prizeItem.prizeImage.indexOf(';base64,') === -1) {
                console.log('开始处理本地图片', prizeItem.prizeImage)
                prizeItem.prizeImage = await pathToBase64(prizeItem.prizeImage)
                console.log('处理本地图片结束', prizeItem.prizeImage)
              }
							// #endif
              
              // #ifdef MP-WEIXIN
              // 小程序环境,把 base64 处理成小程序的本地临时路径
              if (prizeItem.prizeImage.indexOf(';base64,') !== -1) {
                console.log('开始处理BASE64图片', prizeItem.prizeImage)
                prizeItem.prizeImage = await base64ToPath(prizeItem.prizeImage)
                console.log('处理BASE64图片完成', prizeItem.prizeImage)
              }
              // #endif
						}
            
            let prizeImageX = -(this.imgPxWidth * systemInfo.pixelRatio / 2)
            let prizeImageY = this.imgMarginPxStr * systemInfo.pixelRatio
            let prizeImageW = this.imgPxWidth * systemInfo.pixelRatio
            let prizeImageH = this.imgPxHeight * systemInfo.pixelRatio
            ctx.drawImage(prizeItem.prizeImage, prizeImageX, prizeImageY, prizeImageW, prizeImageH)
          }

          ctx.restore()
        }

        // 保存绘图并导出图片
        ctx.draw(true, () => {
          let drawTimer = setTimeout(() => {
            clearTimeout(drawTimer)
            drawTimer = null

            // #ifdef MP-ALIPAY
            ctx.toTempFilePath({
              destWidth: this.higtCanvasSize,
              destHeight: this.higtCanvasSize,
              success: (res) => {
                // console.log(res.apFilePath)
                this.handlePrizeImg({
									ok: true,
									data: res.apFilePath,
									msg: '画布导出生成图片成功'
								})
              },
							fail: (err) => {
                this.handlePrizeImg({
									ok: false,
									data: err,
									msg: '画布导出生成图片失败'
								})
							}
            })
            // #endif
            
            // #ifndef MP-ALIPAY
            uni.canvasToTempFilePath({
              canvasId: this.canvasId,
              success: (res) => {
                // 在 H5 平台下,tempFilePath 为 base64
                // console.log(res.tempFilePath)
                this.handlePrizeImg({
									ok: true,
									data: res.tempFilePath,
									msg: '画布导出生成图片成功'
								})
              },
							fail: (err) => {
                this.handlePrizeImg({
									ok: false,
									data: err,
									msg: '画布导出生成图片失败'
								})
							}
            }, this)
            // #endif
          }, 500)
        })
      },
      // 处理导出的图片
      handlePrizeImg(res) {
				if (res.ok) {
					let data = res.data
					
					if (!this.canvasCached) {
						this.lotteryImg = data
						this.handlePrizeImgSuc(res)
						return
					}
					
					// #ifndef H5
					if (this.isCacheImg) {
						uni.getSavedFileList({
							success: (sucRes) => {
								let fileList = sucRes.fileList
								// console.log('getSavedFileList Cached', fileList)
								
								let cached = false
                
                if (fileList.length) {
                  for (let i = 0; i < fileList.length; i++) {
                  	let item = fileList[i]
                  	if (item.filePath === data) {
                  		cached = true
                  		this.lotteryImg = data
                  		
                  		console.info('经查,本地缓存中存在的转盘图可用,本次将不再绘制转盘')
                  		this.handlePrizeImgSuc(res)
                  		break
                  	}
                  }
                }
								
								if (!cached) {
									console.info('经查,本地缓存中存在的转盘图不可用,需要重新初始化转盘绘制')
									this.initCanvasDraw()
								}
							},
							fail: (err) => {
								this.initCanvasDraw()
							}
						})
					} else {
						uni.saveFile({
							tempFilePath: data,
							success: (sucRes) => {
								let filePath = sucRes.savedFilePath
								// console.log('saveFile', filePath)
								setStore(`${this.canvasId}LotteryImg`, filePath)
								this.lotteryImg = filePath
								this.handlePrizeImgSuc({
									ok: true,
									data: filePath,
									msg: '画布导出生成图片成功'
								})
							},
							fail: (err) => {
								this.handlePrizeImg({
									ok: false,
									data: err,
									msg: '画布导出生成图片失败'
								})
							}
						})
					}
					// #endif
					// #ifdef H5
					setStore(`${this.canvasId}LotteryImg`, data)
					this.lotteryImg = data
					this.handlePrizeImgSuc(res)
          
          // console info
          let consoleText = this.isCacheImg ? '缓存' : '导出'
          console.info(`当前为 H5 端,使用${consoleText}中的 base64 图`)
					// #endif
				} else {
					console.error('处理导出的图片失败', res)
					uni.hideLoading()
					// #ifdef H5
					console.error('###当前为 H5 端,下载网络图片需要后端配置允许跨域###')
					// #endif
					// #ifdef MP
					console.error('###当前为小程序端,下载网络图片需要配置域名白名单###')
					// #endif
				}
      },
			// 处理图片完成
			handlePrizeImgSuc (res) {
				uni.hideLoading()
				this.$emit('finish', {
					ok: res.ok,
					data: res.data,
					msg: res.ok ? this.successMsg : this.failMsg
				})
        this.almostLotteryTip = res.ok ? this.successMsg : this.failMsg
			},
      // 兼容 app 端不支持 ctx.measureText
      // 已知问题:初始绘制时,低端安卓机 平均耗时 2s
      // hbx 2.8.12+ 已在 app 端支持
      getTextWidth() {
        console.warn('正在采用兼容方式获取文本的 size 信息,虽然没有任何问题,如果可以,请将此 systemInfo 及 hbx 版本号 反馈给作者', systemInfo)
        let query = uni.createSelectorQuery().in(this)
        let nodesRef = query.select('.almost-lottery__measureText')
        return new Promise((resolve, reject) => {
          nodesRef.fields({
            size: true,
          }, (res) => {
            resolve(res.width)
          }).exec()
        })
      },
      // 处理文字溢出
      strLimit(value) {
        let maxLength = this.strMaxLen
        if (!value || !maxLength) return value
        return clacTextLen(value).realLen > maxLength ? value.slice(0, maxLength - 1) + '..' : value
      },
			// 检查本地缓存中是否存在转盘图
			checkCacheImg () {
				console.log('检查本地缓存中是否存在转盘图')
				// 检查是否已有缓存的转盘图
				// 检查是否与本次奖品数据相同
				this.oldLotteryImg = getStore(`${this.canvasId}LotteryImg`)
				let oldPrizeList = getStore(`${this.canvasId}PrizeList`)
				let newPrizeList = JSON.stringify(this.prizeList)
				if (this.oldLotteryImg) {
					if (oldPrizeList === newPrizeList) {
						console.log(`经查,本地缓存中存在转盘图 => ${this.oldLotteryImg}`)
						this.isCacheImg = true
						
						console.log('需要继续判断这张缓存图是否可用')
						this.handlePrizeImg({
							ok: true,
							data: this.oldLotteryImg,
							msg: '画布导出生成图片成功'
						})
						return
					}
				}
				
				console.log('经查,本地缓存中不存在转盘图')
				this.initCanvasDraw()
			},
      // 初始化绘制
      initCanvasDraw () {
				console.log('开始初始化转盘绘制')
				this.isCacheImg = false
				this.lotteryImg = ''
				clearStore(`${this.canvasId}LotteryImg`)
        setStore(`${this.canvasId}PrizeList`, this.prizeList)
        this.onCreateCanvas()
      },
      // 预处理初始化
      async beforeInit () {
        // 处理 rpx 自适应尺寸
        let lotterySize = await new Promise((resolve) => {
          uni.createSelectorQuery().in(this).select('.almost-lottery__wrap').boundingClientRect((rects) => {
            resolve(rects)
            // console.log('处理 lottery rpx 的自适应', rects)
          }).exec()
        })
        let actionSize = await new Promise((resolve) => {
          uni.createSelectorQuery().in(this).select('.lottery-action').boundingClientRect((rects) => {
            resolve(rects)
            // console.log('处理 action rpx 的自适应', rects)
          }).exec()
        })
        let strMarginSize = await new Promise((resolve) => {
          uni.createSelectorQuery().in(this).select('.str-margin-outside').boundingClientRect((rects) => {
            resolve(rects)
            // console.log('处理 str-margin-outside rpx 的自适应', rects)
          }).exec()
        })
        let imgMarginStr = await new Promise((resolve) => {
          uni.createSelectorQuery().in(this).select('.img-margin-str').boundingClientRect((rects) => {
            resolve(rects)
            // console.log('处理 img-margin-str rpx 的自适应', rects)
          }).exec()
        })
        let imgSize = await new Promise((resolve) => {
          uni.createSelectorQuery().in(this).select('.img-size').boundingClientRect((rects) => {
            resolve(rects)
            // console.log('处理 img-size rpx 的自适应', rects)
          }).exec()
        })
        this.lotteryPxSize = Math.floor(lotterySize.width)
        this.actionPxSize = Math.floor(actionSize.width)
        this.canvasPxSize = this.lotteryPxSize - Math.floor(actionSize.left) + Math.floor(lotterySize.left)
        this.strMarginPxOutside = Math.floor(strMarginSize.left) - Math.floor(lotterySize.left)
        this.imgMarginPxStr = Math.floor(imgMarginStr.left) - Math.floor(lotterySize.left)
        this.imgPxWidth = Math.floor(imgSize.width)
        this.imgPxHeight = Math.floor(imgSize.height)
        
        let stoTimer = setTimeout(() => {
          clearTimeout(stoTimer)
          stoTimer = null
          
          // 判断画板是否设置缓存
          if (this.canvasCached) {
          	this.checkCacheImg()
          } else {
          	this.initCanvasDraw()
          }
          this.transitionDuration = this.duration
        }, 50)
      }
    },
    mounted() {
      this.$nextTick(() => {
        let stoTimer = setTimeout(() => {
          clearTimeout(stoTimer)
          stoTimer = null
          
          this.beforeInit()
        }, 50)
      })
    }
  }
  • 样式 CSS
<style lang="scss" scoped>
  .almost-lottery {
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
  }
  
  .almost-lottery__wrap {
    position: relative;
    // background-color: red;
  }
  .lottery-action,
  .str-margin-outside,
  .img-margin-str,
  .img-size {
    position: absolute;
    left: 0;
    top: 0;
    z-index: -1;
    // background-color: blue;
  }
  
	.almost-lottery__wrap {
    display: flex;
    justify-content: center;
    align-items: center;
	}

  .almost-lottery__action,
  .almost-lottery__bg,
  .almost-lottery__canvas {
    position: absolute;
  }

  .almost-lottery__canvas {
    left: -9999px;
    opacity: 0;
    display: flex;
    justify-content: center;
    align-items: center;
  }

  .almost-lottery__canvas-img,
  .almost-lottery__action-bg {
		display: block;
    transition: transform cubic-bezier(.34, .12, .05, .95);
  }

  .almost-lottery__tip {
    color: #FFFFFF;
    font-size: 24rpx;
    text-align: center;
  }
  
  .almost-lottery__measureText {
    position: absolute;
    left: 0;
    top: 0;
    white-space: nowrap;
    font-size: 12px;
    opacity: 0;
  }
</style>

** 这个是封装好的组件

  • 使用
  • 1.引入
  • 2.调用
<almost-lottery
        :lottery-size="lotteryConfig.lotterySize"
        :action-size="lotteryConfig.actionSize"
        :ring-count="2"
        :duration="1"
        :prize-list="prizeList"
        :prize-index="prizeIndex"
        @reset-index="prizeIndex = -1"
        @draw-start="handleDrawStart"
        @draw-end="handleDrawEnd"
        @finish="handleDrawFinish"
        v-if="prizeList.length"
      />

组件介绍

// 以下是转盘配置相关数据
        lotteryConfig: {
          // 抽奖转盘的整体尺寸,单位rpx
          lotterySize: 600,
          // 抽奖按钮的尺寸,单位rpx
          actionSize: 200
        },
        
        // 以下是转盘 UI 配置
        // 转盘外环图,如有需要,请参考替换为自己的设计稿
        lotteryBg: '/static/lottery-bg.png',
        // 抽奖按钮图
        actionBg: '/static/action-bg.png',
        
        // 以下是奖品配置数据
        // 奖品数据
        prizeList: [],
				// 奖品是否设有库存
				onStock: true,
        // 中奖下标
        prizeIndex: -1,
        
        // 是否正在抽奖中,避免重复触发
        prizeing: false,
        
        // 以下为中奖概率有关数据
        // 是否由前端控制概率,默认不开启,强烈建议由后端控制
        onFrontend: false,
        // 权重随机数的最大值
        prizeWeightMax: 0,
        // 权重数组
        prizeWeightArr: [],
        
        // 以下为业务需求有关示例数据
        // 金币余额
        goldCoin: 600,
        // 当日免费抽奖次数余额
        freeNum: 3,
        // 每次消耗的金币数
        goldNum: 20,
        // 每天免费抽奖次数
        freeNumDay: 3
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

uniapp实现抽奖功能 的相关文章

  • ubuntu 安装rtorrent 下载

    apt get install rtorrent 在根目录下 建立 rtorrent rc 最小允许peer数 min peers 3 最大允许peer数 max peers 500 最大同时上传用户数 max uploads 10 最大下
  • 数据结构7/23—链表实现简单的学生信息管理系统

    目录 定义结构体存储学生信息 姓名 成绩 功能实现 各种功能函数如下 菜单函数 创建链表函数 判断是否为空的判空函数 插入函数 头插方式实现 遍历链表函数 排序输出 升序 输出最高分学生信息 以名字查找该学生的前驱节点 通过名字删除学生信息

随机推荐

  • Flutter Cocoon 已达到 SLSA 2 级标准的要求

    文 Jesse Seales Dart 和 Flutter 安全工作组工程师 今年年初 我们发布了 Flutter 2022 产品路线图 其中 基础设施建设 这部分提到 2022 年 Flutter 团队将增加对供应链的安全的投入 目的是达
  • PHP启动warning:PHP Startup: Unable to load dynamic library 'curl.so'

    高通ar9531上面 openwrt1806这个版本 通过opkg安装了官方的php及其扩展 但新的板子php启动的时候报了warning 没有太仔细看 但是后面运行cgi程序时 发现了问题 回头看warning日志 PHP Warning
  • Android Socket 简单介绍

    文章目录 前言 一 Socket是什么 百度百科的解释 我自己的理解 二 简单示例 1 服务端 2 客户端 3 布局 4 实现 参考 总结 前言 最近需求需要使用Socket进行通讯 我在工作后的安卓开发中没有接触过 所以有了这篇文章 写的
  • 《每日一题》NO.41:FPGA内部资源有哪些?

    芯司机 每日一题 会每天更新一道IC面试笔试题 其中有些题目已经被很多企业参考采用了哦 聪明的你快来挑战一下吧 今天是第41题 FPGA设计工程师也是一个比较热门的职位 FPGA中都包括哪些资源呢 今天的题就是这样啦 开始解题吧 公布答案
  • 内窥镜胶囊(胶囊内镜)硬件方案

    内窥镜胶囊 胶囊内镜 胶囊内窥镜 硬件方案 前言 说明 该方案为作者2018年上半年完成的第一版 后来搁置了一段时间 才重启这个项目 目前 2020 07 第二版已经快要完成 联系v 1 7 6 3 3 3 5 0 8 7 0 先给一下第一
  • Visual Studio Code结合Git与GitHub的完整步骤

    一 Visual Studio Code安装 官网下载地址 https code visualstudio com Visual Studio Code是一个精简版的迷你Visual Studio 并且可以跨平台 Windows Mac L
  • centos7.6安装mysql

    卸载mariadb 解决安装mysql与mariadb冲突问题 卸载干净mariadb 何妨徐行的博客 CSDN博客 安装rpm包前可能需要的命令 yum install openssl devel用于管理rpm包的工具 yum insta
  • 雪崩 计算机组成原理,计算机组成原理复习资料(学习课件整理版可自学使用).doc...

    一 本课程在计算机系统中的位置 一 课程目标 1 结构与原理掌握 建立计算机系统的整机概念 掌握计算机各部件的组成原理与技术 了解计算机系统组成与结构的新技术 2 分析与计算能力 掌握对组成与结构进行性能分析的方法 通过量化计算 加深对组成
  • 李沐动手学深度学习V2-目标检测SSD

    一 目标检测SSD 单发多框检测 1 介绍 SSD模型主要由基础网络组成 其后是几个多尺度特征块 基本网络用于从输入图像中提取特征 因此它可以使用深度卷积神经网络 单发多框检测论文中选用了在分类层之前截断的VGG 现在也常用ResNet替代
  • css中hover变大效果

    html代码 div img src img 11 jpg alt div css代码
  • 轻试Nginx的负载均衡

    看到网上的负载均衡 一直都没有怎么看过 也不理解 今天从网上学着点在windows下用Nginx来试试 我的os是windows xp 用的web服务器时IIS5 1 用了两台同在一个局域网的电脑 分别装有IIS 作为web服务器 地址为1
  • Linux如何查找杀死僵死进程

    最近工作过程中 发现好几台服务器出现僵死进程 如图 用下面的命令找出僵死进程 ps A o stat ppid pid cmd grep e Zz 命令注解 A 参数列出所有进程 o 自定义输出字段 我们设定显示字段为 stat 状态 pp
  • 数组随机排序的两种实现方法

    1 利用数组中的sort 方法 arr arr sort gt Math random 0 5 2 Fisher Yates Shuffle 复杂度为O n 从后向前遍历 不断将当前元素与随机位置的元素 除去已遍历的部分 进行交换 func
  • 海尔对话 Unity:作为数字转型的高阶形态,数字孪生发展前景不可逆

    来源 数字化企业 作为信息化发展到一定程度的必然结果 数字孪生正成为人类解构 描述和认识真实世界和虚拟世界的新型工具 从发展态势来看 数字孪生不仅是全新信息技术发展的新焦点 也是各国实现数字化转型的新抓手 还是众多工业企业业务布局的新方向
  • 第4章 微服务框架主体搭建

    mini商城第4章 微服务框架主体搭建 一 课题 框架搭建 二 回顾 1 整体业务功能分析 2 根据业务需求设计表结构及字段 三 目标 1 版本控制器的搭建使用 2 能独立自主的搭建微服务框架 3 学会考虑一些公共的工具组件 4 网关模块的
  • 树莓派初始使用相关问题及解决方法

    1 SSH连接 网线连接电脑 在无线图标上右键 打开 网络和internet设置 更改适配器设置 在WLAN上面右键 选择属性 共享 勾上 在cmd里面 可以输入 ping raspberrypi local 若显示不是ip 则ping 4
  • opencv3/C++ 机器学习-最邻近算法KNN识别字符

    如图 有如下字母表 现尝试采用最邻近算法KNN 取前10列字符作为训练数据 然后识别字母表中的字符 创建训练数据 首先通过获取前10列字符的轮廓外接矩形 将字符裁剪出作为训练样本建立图库 include
  • 【R语言】富集分析可视化代码(整理版)

    前面几期 干货预警 原来基因功能富集分析这么简单 R语言 基因GO KEGG功能富集结果可视化 保姆级教程 和 R语言 基因GO KEGG功能富集分析 超级简单的保姆级教程 分别介绍了如何使用DAVID在线分析工具对基因进行GO KEGG功
  • 谷歌开源项目Chromium的源码获取与项目构建(Win7+vs10/vs13)

    从12年那会儿开始获取源码和构建chromium项目都是按照那时候的官方要求用win7 vs2010 相对来说也比较简单 按照步骤来也很快能编译出来 1 官网的编译配置介绍 http www chromium org developers
  • uniapp实现抽奖功能

    效果 代码