手撸简易大转盘抽奖组件

2,239 阅读2分钟

一、画圆盘

实现思路:

  • 根据奖品数量将圆盘等分,计算出一份奖品所占的角度和大致宽度
  • 将每一格奖品底边的中点设置在圆盘的原点处
  • 根据顺序计算每一格奖品所需要旋转的角度,并旋转至各自的位置,铺满360度圆盘
  • 将指针定位至圆盘中心处

奖品数据

const prizeList = [
  {
      name: "奖品1",
      color: "red"
  },
  {
      name: "奖品2",
      color: "orange"
  },
  {
      name: "奖品3",
      color: "yellow"
  },
  {
      name: "奖品4",
      color: "green"
  },
  {
      name: "奖品5",
      color: "hotpink"
  },
  {
      name: "奖品6",
      color: "blue"
  },
  {
      name: "奖品7",
      color: "purple"
  },
  {
      name: "奖品8",
      color: "gray"
  }
]
data() {
   return {
      prizeLists: [],
      angleList: []
    }
}

计算旋转角度

initPrizeList() {
	const length = this.prizeList.length // 奖品数量
    const width = parseInt(3.14 * 300 / length) // 每份奖品的大致宽度,由于抽奖指针最终只会定格在奖品的最中间位置,所以这里只需计算大致宽度即可,有重叠部分对页面视觉和结果无影响
    const halfWidth = -parseInt(width / 2) // 奖品初始位置需要偏离的值
    const average = 360 / length // 均分圆盘,每份奖品所占的角度
    const half = average / 2 // 每份奖品的中间位置,也是指针最终停留的角度
    list.forEach((item, index) => {
    	const angle = - (index * average + half) // 依次计算每份奖品的旋转角度,初始位置都是朝向12点方向
        item.style = `transform: rotate(${angle}deg); width: ${width}px; marginLeft: ${halfWidth}px` // 首先将奖品全部移动到"下边的中点至圆盘原点"的状态,再旋转到上一步计算出来的角度
        this.angleList.push(index * average + half) // 收集每个奖品中间位置的角度
    })
    return [...list]
}

页面结构

<div class="lottery-wrap">
    <img class="lottery-pointer" :src="pointerImg" @click="beginRotate" alt="">
    <div class="prize-wrap" :style="rotateStyle">
        <div class="prize-list">
            <div class="prize-item" :style="item.style" v-for="(item, index) in prizeLists" :key="index">
                <span class="prize-icon" :style="{backgroundColor: item.color}"></span>
                <span class="prize-name">{{item.name}}</span>
            </div>
        </div>
    </div>
</div>
.lottery-wrap {
    width: 300px;
    height: 300px;
    border-radius: 50%;
    background-color: rgba(0,0,0,0.4);
    position: relative;
}
.lottery-pointer {
    width: 100px;
    height: 100px;
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    z-index: 2;
}
.prize-wrap {
    width: 100%;
    height: 100%;
}
.prize-list {
    position: absolute;
    width: 100%;
    height: 100%;
    top: 0;
    left: 0;
    .prize-item {
        position: absolute;
        // width: 120px;
        height: 150px;
        box-sizing: border-box;
        padding-top: 30px;
        top: 0;
        left: 50%;
        // margin-left: -60px;
        transform-origin: 50% 100%;
        display: flex;
        flex-direction: column;
        // justify-content: center;
        align-items: center;
        .prize-icon {
            width: 30px;
            height: 30px;
        }
    }
}

至此圆盘的结构就完成了

二、转起来

实现思路:

  • 用户点击抽奖后,只需要转动圆盘,指针保持不动即可
  • 真实场景中获取后端返回的奖项后,前端只需计算出角度,再执行旋转动画即可,
  • 每次计算处新角度,可以通过计算属性来让页面更新
  • 需要注意的是每次旋转完要保存旋转角度,并且防止用户重复点击

初始数据

data() {
	isRotating: false, // 防止重复点击旋转
    rotateAngle: 0 // 当前位置
}

旋转方法

methods: {
	beginRotate() {
    	const { isRotating, rotateAngle, angleList } = this
        if (isRotating) reurn // 防止重复点击
        this.isRotating = true
        const angle = rotateAngle + 5 * 360 + angleList[4] - (rotateAngle % 360) // 为了视觉好看,这里默认旋转5圈以上
        this.rotateAngle = angle // 保存此次旋转角度,下次旋转需要用到
        
        setTimeout(() => {
        	this.isRotating = false // 本次抽奖结束,可以进行下一次
            console.log("抽中了!)
        }, 5000)
    }
}

计算属性更新页面

computed: {
	rotateStyle() {
    	return `
            transition: transform 5000ms ease-in-out;
            transform: rotate(${this.rotateAngle}deg)
        `
    }
}

至此完成了转盘的抽奖功能

接下来改造成可配置性更高的组件

三、改造成可配置的组件

实现思路:尽可能将可配置的数据改造成参数传入,包含背景图、指针图片、奖品列表、旋转时间、动画效果、旋转圈数等

接收参数

props: {
	prizeList: {
		type: Array,
        default: () => []
    },
    pointerImg: {
    	type: String,
        default: () => require("./img/xxx.png")
    },
    lotteryBgImg: {
    	type: String,
        default: () => require("./img/xxx.png")
    },
    duration: {
    	type: Number,
        default: () => 5000
    },
    circle: {
    	type: Number,
        default: () => 5
    },
    mode: {
    	type: String,
        default: () => "ease-in-out"
    }
}

旋转方法改造

methods: {
	rotating(index) {
    	const { isRotating, duration, circle, count, rotateAngle, angleList } = this
        if (isRotating) reurn // 防止重复点击
        this.isRotating = true
        const angle = rotateAngle + circle * 360 + angleList[index] - (rotateAngle % 360) // 为了视觉好看,这里默认旋转5圈以上
        this.rotateAngle = angle // 保存此次旋转角度,下次旋转需要用到
        
        setTimeout(() => {
        	this.isRotating = false // 本次抽奖结束,可以进行下一次
            console.log("抽中了!)
        }, duration)
    }
}

计算属性改造

computed: {
	rotateStyle() {
    	return `
            transition: transform ${this.duration}ms ${this.mode};
            transform: rotate(${this.rotateAngle}deg)
        `
    }
}

实际场景中,最终获奖奖品一般由后端控制比较安全,因此这里需要改造成由父组件来控制, 而点击事件是在自组件中,所以做如下改造

<div class="lottery-wrap">
    <img class="lottery-pointer" :src="pointerImg" @click="beginRotate" alt="">
    <div class="prize-wrap" :style="rotateStyle">
        <div class="prize-list">
            <div class="prize-item" :style="item.style" v-for="(item, index) in prizeLists" :key="index">
                <span class="prize-icon" :style="{backgroundColor: item.color}"></span>
                <span class="prize-name">{{item.name}}</span>
            </div>
        </div>
    </div>
</div>
methods: {
	beginRotate() {
    	this.$emit("beginRotate") // 让父组件来控制点击,并传入获奖结果
    },
    rotating(index) {
    	const { isRotating, duration, circle, count, rotateAngle, angleList } = this
        if (isRotating) reurn // 防止重复点击
        this.isRotating = true
        const angle = rotateAngle + circle * 360 + angleList[index] - (rotateAngle % 360) // 为了视觉好看,这里默认旋转5圈以上
        this.rotateAngle = angle // 保存此次旋转角度,下次旋转需要用到
        
        setTimeout(() => {
        	this.isRotating = false // 本次抽奖结束,可以进行下一次
            this.rotateOver()
        }, duration)
    },
    rotateOver() {
    	this.$emit("rotateOver") // 抽奖结束,通知父组件
    }
}
// 父组件
<turntable ref="lottery" :prizeList="prizeList" @beginRotate="beginRotate" @rotateOver="rotateOver"></turntable>
methods: {
	beginRotate() {
    	// 这里调用后端接口拿到最终获奖结果
        const res = .......
        this.$refs.lottery.rotating(res)
    },
    rotateOver() {
    	// 完成抽奖结束的剩余步骤
    }
}

至此,就完成简易功能的抽奖组件