效果

  • 代码
<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"><imageclass="almost-lottery__bg"mode="widthFix":src="lotteryBg":style="{width: lotteryPxSize + 'px',height: lotteryPxSize + 'px'}"></image><imageclass="almost-lottery__canvas-img"mode="widthFix":src="lotteryImg":style="{width: canvasPxSize + 'px',height: canvasPxSize  + 'px',transform: `rotate(${canvasAngle + targetAngle}deg)`,transitionDuration: `${transitionDuration}s`}"></image><imageclass="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'},// 旋转动画时间 单位sduration: {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 {// 画板classNameclassName: '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,// 旋转动画时间 单位 stransitionDuration: 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 = 0let prizeCount = this.prizeList.lengthlet prizeClip = 360 / prizeCountlet diffNum = 90 / prizeClipif (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 = newValthis.onRotateStart()} else {console.info('旋转结束,prizeIndex 已重置')}}},methods: {// 开始旋转onRotateStart() {if (this.isRotate) returnthis.isRotate = true// 奖品总数let prizeCount = this.prizeList.lengthlet baseAngle = 360 / prizeCountlet angles = 0if (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.ringCountconsole.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 + 100let endTimer = setTimeout(() => {clearTimeout(endTimer)endTimer = nullthis.isRotate = falsethis.$emit('draw-end')}, endTime)let resetPrizeTimer = setTimeout(() => {clearTimeout(resetPrizeTimer)resetPrizeTimer = null// 每次抽奖结束后都要重置父级组件的 prizeIndexthis.$emit('reset-index')}, endTime + 50)},// 点击 开始抽奖 按钮handleActionStart() {if (!this.lotteryImg) returnif (this.isRotate) returnthis.$emit('draw-start')},// 渲染转盘async onCreateCanvas() {// 获取 canvas 画布const canvasId = this.canvasIdconst ctx = uni.createCanvasContext(canvasId, this)// canvas 的宽高let canvasW = this.higtCanvasSizelet canvasH = this.higtCanvasSize// 根据奖品个数计算 角度let prizeCount = this.prizeList.lengthlet 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.textDistancelet translateY = canvasH * 0.5 + Math.sin(angle + baseAngle / 2) * this.textDistancectx.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).realLenlet isLineBreak = realLen > this.strLineLenif (isLineBreak) {// 获得多行文本数组let firstText = ''let lastText = ''let firstCount = 0for (let j = 0; j < rewardName.length; j++) {firstCount += clacTextLen(rewardName[j]).byteLenif (firstCount <= (this.strLineLen * 2)) {firstText += rewardName[j]} else {lastText += rewardName[j]}}rewardName = firstText + ',' + lastTextlet 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/103303let reg = /^(https|http)/g// 处理远程图片if (reg.test(prizeItem.prizeImage)) {let platformTips = ''// #ifdef APP-PLUSplatformTips = ''// #endif// #ifdef MPplatformTips = '需要处理好下载域名的白名单问题,'// #endif// #ifdef H5platformTips = '需要处理好跨域问题,'// #endifconsole.warn(`###当前数据列表中的奖品图片为网络图片,${platformTips}开始尝试下载图片...###`)let res = await downloadFile(prizeItem.prizeImage)console.log('处理远程图片', res)if (res.ok) {let tempFilePath = res.tempFilePath// #ifndef MPprizeItem.prizeImage = await pathToBase64(tempFilePath)// #endif// #ifdef MPprizeItem.prizeImage = tempFilePath// #endif} else {this.handlePrizeImgSuc({ok: false,data: res.data,msg: res.msg})}} else {// #ifndef MP// 不是小程序环境,把本地图片处理成 base64if (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.pixelRatiolet prizeImageW = this.imgPxWidth * systemInfo.pixelRatiolet prizeImageH = this.imgPxHeight * systemInfo.pixelRatioctx.drawImage(prizeItem.prizeImage, prizeImageX, prizeImageY, prizeImageW, prizeImageH)}ctx.restore()}// 保存绘图并导出图片ctx.draw(true, () => {let drawTimer = setTimeout(() => {clearTimeout(drawTimer)drawTimer = null// #ifdef MP-ALIPAYctx.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-ALIPAYuni.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.dataif (!this.canvasCached) {this.lotteryImg = datathis.handlePrizeImgSuc(res)return}// #ifndef H5if (this.isCacheImg) {uni.getSavedFileList({success: (sucRes) => {let fileList = sucRes.fileList// console.log('getSavedFileList Cached', fileList)let cached = falseif (fileList.length) {for (let i = 0; i < fileList.length; i++) {let item = fileList[i]if (item.filePath === data) {cached = truethis.lotteryImg = dataconsole.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 = filePaththis.handlePrizeImgSuc({ok: true,data: filePath,msg: '画布导出生成图片成功'})},fail: (err) => {this.handlePrizeImg({ok: false,data: err,msg: '画布导出生成图片失败'})}})}// #endif// #ifdef H5setStore(`${this.canvasId}LotteryImg`, data)this.lotteryImg = datathis.handlePrizeImgSuc(res)// console infolet consoleText = this.isCacheImg ? '缓存' : '导出'console.info(`当前为 H5 端,使用${consoleText}中的 base64 图`)// #endif} else {console.error('处理导出的图片失败', res)uni.hideLoading()// #ifdef H5console.error('###当前为 H5 端,下载网络图片需要后端配置允许跨域###')// #endif// #ifdef MPconsole.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.strMaxLenif (!value || !maxLength) return valuereturn 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 = trueconsole.log('需要继续判断这张缓存图是否可用')this.handlePrizeImg({ok: true,data: this.oldLotteryImg,msg: '画布导出生成图片成功'})return}}console.log('经查,本地缓存中不存在转盘图')this.initCanvasDraw()},// 初始化绘制initCanvasDraw () {console.log('开始初始化转盘绘制')this.isCacheImg = falsethis.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 = nullthis.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: {// 抽奖转盘的整体尺寸,单位rpxlotterySize: 600,// 抽奖按钮的尺寸,单位rpxactionSize: 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

uniapp实现抽奖功能相关推荐

  1. uniapp 九宫格抽奖功能

    <template><view class="bg"><view class="container"><view cl ...

  2. 给你30秒的时间,你会用Excel制作出一个抽奖功能吗?

    一说到抽奖,大家都是想到最近的"支付宝锦鲤信小呆",但是今天跟大家谈论的不是这个,而是:你会用Excel制作抽奖功能吗?可能大家都不知道Excel有这么多的神技能,但是你不知道的还 ...

  3. JSP常用内置对象及抽奖功能

    jsp内置对象 JSP九个内置对象分别为:request,response,session,application,config,exception,page,out,pageContext 常用五个 ...

  4. IVX低代码平台开发——微信小程序实现抽奖功能

    写在前面 通过利用可视化编程实现微信小程序的抽奖功能,带大家初步了解 iVX 的强大之处. 文章目录 写在前面 iVX开发 抽奖功能实现 iVX开发 基本介绍 iVX是一个 "零代码&quo ...

  5. php 抽奖活动_PHP实现活动人选抽奖功能的方法

    这篇文章主要介绍了PHP实现活动人选抽奖功能,随机抽取指定人数,依次列举被抽中的人名,具有一定的参考价值,感兴趣的小伙伴们可以参考一下 本文介绍情景为活动人选抽奖,通过简单随机抽取指定人数,依次列举被 ...

  6. uniapp 实现登录功能与获取用户凭证

    uniapp 实现登录功能与获取用户凭证 前言 正文 uniapp 后端 前言 欢迎大佬指正思路. 由于最近面试时,被问到了这种问题,但是我回答的含糊不清. 虽然以前做过这些功能,但是一直都没有去总结 ...

  7. java抽奖_JAVA实现用户抽奖功能(附完整代码)

    需求分析 1)实现三个基本功能:登录.注册.抽奖. 2)登录:用户输入账号密码进行登录,输入账号后会匹配已注册的用户,若输入用户不存在则退出,密码有三次输入机会,登录成功后主界面会显示已登录用户的账号 ...

  8. Java实现抽奖功能

    这篇文章主要为大家详细介绍了Java实现抽奖功能,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下 本文实例为大家分享了Java实现抽奖功能的具体代码,供大家参考,具体内容 ...

  9. php jq实现抽奖,php 实现抽奖功能

    最近做的小程序有一个抽奖功能,这里记录下实现抽奖功能的具体流程 前提: 有一组奖品数据如下 id 奖品(prize) 概率(rate) 数量(num) 已抽数量(prize_num) 1 一等奖 10 ...

最新文章

  1. 服务器返回数据为空,iOS 处理服务器返回数据中的null
  2. SAP MB1B + 313315做二步法货物移动报错-创建交货的数据不完全(客户)-
  3. boost库在工作(20)线程之五
  4. 57. Leetcode 257. 二叉树的所有路径 (二叉树-二叉树路径和)
  5. UITableView的UITableViewStyleGrouped
  6. 安装mysql-connector-python-8.0.11-py3.6遇到问题
  7. MySQL入门之创建、更新、修改、复制、查看表
  8. Github新安全措施:停止Git客户端账号密码登录的解决方案
  9. 7-16 装箱问题 (20 分)
  10. 面试自我介绍和简历上的内容能不能相同?
  11. 数据结构与算法 实验5 树、二叉树和森林的基本操作
  12. BP神经网络隐含层节点数的确定
  13. 桌面虚拟化技术 KVM
  14. Code Clinic: Clojure 代码诊所:Clojure Lynda课程中文字幕
  15. 对 Android 开发者有益的 40 条优化建议
  16. 简单易学的机器学习算法——受限玻尔兹曼机RBM
  17. 高仿网易新闻栏目动画效果
  18. 网络层(TCP/UDP)攻击与防御原理
  19. python小游戏实现代码
  20. kettle利用时间戳(timestamp)做增量抽取

热门文章

  1. R语言使用qweibull函数生成威布尔(韦伯分布)分布分位数函数数据、使用plot函数可视化威布尔分布分位数函数数据(Weibull Distribution)
  2. 华为鸿蒙OS终端荣耀智慧屏,全球首款鸿蒙OS终端荣耀智慧屏正式发布
  3. flink 教程 Window
  4. 智工运维定位器技术方案及选型
  5. 雅诗兰黛公司启用旅游零售渠道专用的加尔盖嫩先进分销中心,加强全球履约网络
  6. 【数据结构】数据结构练习题5——查找+排序
  7. 学计算机的kaocpa,注册会计师考试CPA要避开四种假象
  8. 图像拼接小实验开发日志和笔记
  9. 同学揭周鸿祎在西安交大时期的传奇经历
  10. [自学第十一天] 静态项目实战_纽曼官网(用时三天)