Vue.js学习笔记(五)抽奖组件封装——转盘抽奖

基于VUE2转盘组件的开发


前言

因为之前的转盘功能是图片做的,每次活动更新都要重做UI和前端,为了解决这一问题进行动态配置转盘组件开发,可以减少一些UI和前端的工作量。项目使用vue脚手架+vant组件库进行开发。


一、开发步骤

1.组件布局

 <van-row class="container">
 	  <!-- turntableBox 为整个转盘容器,为正方形,大小由里面元素决定 -->
      <van-col span="24" class="turntableBox">
      	<!-- turntableMain 为转盘底座,比里面的内容大,显示为效果图灰色外圈,但不是空心圆 -->
        <div class="turntableMain" :style="`height:${window.innerWidth * 0.8}px;width:${window.innerWidth * 0.8}px;`">
         <!-- turntable 为转动区域,作用是为了不让外圈一起转动 -->
          <div ref="turntable" class="turntable"
            :style="`height:${window.innerWidth * 0.8}px;width:${window.innerWidth * 0.8}px;`">
           <!-- Canvas 转盘饼图背景,具体划分多少块由奖项决定 -->
            <Canvas />
            <!-- prizeBox 奖项,高为饼图的半径,宽为饼图半径里面有多少块就多少分之一 -->
            <div class="prizeBox">
              <div class="prizeItem" :style="`width:${perPrize.width}px;height:${perPrize.height}px;transform:translateX(-50%) rotate(-${(perPrize.degree * (index + 1)) - (perPrize.degree / 2)}deg);left:calc(50%)`"
                v-for="(item, index) in activeInfo.prizeList" :key="index">
                <p class="title">{{ item.name }}</p>
                <p class="describe">{{ item.describe }}</p>
                <img :src="item.img" style="width: 38%;" />
              </div>
            </div>
          </div>
          <!-- 启动按钮 -->
          <van-image class="go" fit="cover" width="42px" :src="goPointer" @click="go" />
        </div>
      </van-col>
      <!-- 结果展示列表 -->
      <van-col span="24">
        <div id="result"></div>
      </van-col>
    </van-row>

2.布局样式

.turntableBox {
 
  margin-top: 10%;

  .turntableMain {
    margin: 0 auto;
    position: relative;
    border: 10px solid #E5E5E5;
    border-radius: 100%;
  }

  .turntable {
    transition: all 4s;
    margin: 0 auto;
  }

  .go {
    position: absolute;
    top: calc(50% - 31px);
    left: calc(50% - 21px);
  }

  .prizeBox {
    position: absolute;
    width: 80%;
    top: 0;
    left: calc(50% - 40%);

    .prizeItem {
      text-align: center;
      position: absolute;
      top: 0;
      overflow: hidden;
      text-align: center;
      transform-origin: center bottom;
      transform: translateX(-50%);
      color: #2c3e50;

      p {
        margin: 0;
        padding: 0;
      }

      .title {
        font-size: 18px;
        margin-top: 12px;
      }

      .describe {
        font-size: 14px;
        line-height: 28px;
        white-space: break-spaces;
      }

      img {
        margin-top: 6px;
      }
    }
  }
}

3.数据准备

data 代码如下:包含页面功能所需要的变量

  data() {
    return {
      window,
       /** 活动设置 */
      activeInfo: {
        /** 中奖概率 */
        probabilities: {
          "一等奖": 10,
          "二等奖": 10,
          "三等奖": 10,
          "四等奖": 10,
        },
        /** 奖品信息 */
        prizeList: [
          {
            name: '一等奖',
            describe: '一等奖',
            img: 'https://img01.yzcdn.cn/vant/cat.jpeg'
          },
          {
            name: '未中奖',
            describe: '未中奖',
            img: 'https://img01.yzcdn.cn/vant/cat.jpeg'
          },
          {
            name: '二等奖',
            describe: '二等奖',
            img: 'https://img01.yzcdn.cn/vant/cat.jpeg'
          },
          {
            name: '未中奖',
            describe: '未中奖',
            img: 'https://img01.yzcdn.cn/vant/cat.jpeg'
          },
          {
            name: '三等奖',
            describe: '三等奖',
            img: 'https://img01.yzcdn.cn/vant/cat.jpeg'
          },
          {
            name: '四等奖',
            describe: '四等奖',
            img: 'https://img01.yzcdn.cn/vant/cat.jpeg'
          },

        ]
      },
      /** 是否正在执行动画 */
      isGo: false,
      /** 执行动画的对象 */
      oTurntable: '',
      /** 即将旋转的度数 */
      randomDeg: 0,
      /** 上一次旋转的度数 */
      lastDeg: 0,
      /** 抽奖次数 */
      goTimes: 3,
      /** 奖品图片 */
      perPrize: {
        degree: null,
        width: null,
        height: null
      }
     }
   } 

created 代码如下:主要处理角度、宽、高

  created() {
    const params = getAllParams();
    if (params) {
      this.params = params;
    };
    /** 奖品 */
    const angle = (360 / this.activeInfo.prizeList.length) / 2; // 对角角度
    const ratio = Number(Math.sin(angle * (Math.PI * 2 / 360)).toFixed(2)); // 与半径的比率
    this.perPrize = {
      degree: (360 / this.activeInfo.prizeList.length),
      width: Math.floor((window.innerWidth * ratio)) / 2,
      /** 高度是直径的一半 */
      height: window.innerWidth * 0.8 / 2
    }
  },

mounted 代码如下:获取转盘区域DOM元素,方便后面操作

  mounted() {
    this.oTurntable = this.$refs.turntable;
  },

methods 代码如下:主要操作方法

 /** 点击抽奖 */
    go() {
      /** 正在抽奖,未结束继续点击无效 */
      if (!this.isGo && this.goTimes > 0) {
      	/** 获取中奖结果,再根据结果去转动转盘 */
        const result = this.generatePrize();
        /** 
         * 获取奖项下标
         * 奖项名字可能会重复,所以需要找到奖项的所有下标保存到数组里
         * 根据下标数组随机生成一个数字来决定选择哪个下标成为最终结果的下标
         *  */
        const resultIndexArray = this.activeInfo.prizeList.reduce((acc, item, index) => {
          if (item.name === result) {
            acc.push(index);
          }
          return acc;
        }, []);
        const randomResultIndex = Math.floor(Math.random() * resultIndexArray.length);
        const index = resultIndexArray[randomResultIndex];
        /** 奖项总和数量 */
        const length = this.activeInfo.prizeList.length;
        /** 调用旋转方法 */
        this.ratating((360 / length * index) + (360 / length / 2), result);
      }
      else if (!this.isGo && this.goTimes <= 0) {
        this.$toast({
          message: '抱歉,您的抽奖次数用完了',
          duration: 3000,
        });
      }
      else {
        this.$toast('请勿重复点击')
        return
      }
    },
    /** 获取抽奖结果 */
    generatePrize() {
      /** 生成一个 0 到 99 之间的随机数 */
      const randomNum = Math.floor(Math.random() * 100);
      let cumulativeProbability = 0;
      /** 如果概率落在奖项范围内 */
      for (const prize in this.activeInfo.probabilities) {
        cumulativeProbability += this.activeInfo.probabilities[prize];
        if (randomNum < cumulativeProbability) {
          /** 返回中奖内容 */
          return prize;
        }
      }
      // 默认返回未中奖
      return "未中奖";
    },

    /** 该方法能产生[n,m]之间随机数,决定转盘转多少圈 */
    getRandom(n, m) {
      let result = Math.floor(Math.floor(Math.random() * (m - n + 1) + n))
      return result;
    },
    
    /** 旋转 */
    ratating(deg, text) {
      this.goTimes--;
      this.isGo = true;
      /** 旋转圈数 */
      let turnNumber = this.getRandom(3, 6);
      /** 记录这次要旋转的度数(传来的度数+圈数) */
      this.randomDeg = deg + 360 * turnNumber;
      /*上次指针离初始状态的度数 + 上次的度数 + 这次要旋转的度数
      (这样的目的是为了每次旋转都从原点开始,保证数据准确)*/
      let realDeg = (360 - this.lastDeg % 360) + this.lastDeg + this.randomDeg;
      /** 为对象添加执行动画 */
      this.oTurntable.style.transform = `rotate(${realDeg}deg)`;
      setTimeout(() => {
        this.isGo = false;
        var list = document.getElementById('result');
        list.innerHTML += /未中奖/.test(text) ? `<p>很遗憾,您${text}!</p>` : `<p>恭喜您,获得${text}!</p>`;
        /** 把这次度数存储起来,方便下一次获取 */
        this.lastDeg = realDeg;
      }, 4000);
    }

canvas 组件代码如下:主要使用canvas标签根据奖项长度进行角度划分绘画,


<template>
  <canvas class="canvas" id="canvasImg" :style="`width:${perimeter}px;height: ${perimeter}px;`">您的浏览器不支持canvas!</canvas>
</template>

<script>


export default {
  name: 'Canvas',
  components: {

  },
  data() {
    return {
      /** 直径 */
      perimeter: 320,
    }
  },
  created() {

  },
  mounted() {
    this.perimeter = window.innerWidth * 0.8;
    this.drawPie();
  },
  methods: {
    /** 画饼图 */
    drawPie() {
      const PI = Math.PI;
      /** 获取画布并获取2d上下文对象 */
      const canvas = document.getElementById('canvasImg');
      const ctx = canvas.getContext('2d');
      /** 假设周长为500 */
      const perimeter = this.perimeter;
      /** 半径 */
      const radius = perimeter * 0.5;
      /** 总奖品数,需要根据实际数据长度从父组件传入 */
      const prizeTotal = 6;
      /** 每个扇形的角度=360度 / 总奖品数 */
      const degree = 360 / prizeTotal;
      /** 画布宽高 */
      canvas.width = perimeter;
      canvas.height = perimeter;
      /** 根据奖品数把圆形分成等份的扇形 */
      for (let i = 0; i < prizeTotal; i++) {
        /** 奇偶颜色 */
        const color = i % 2 === 0 ? "#F8D383" : "#F8E2BC";
        /** 设置边框颜色 */ 
        const borderColor = "#fff"; 
         /** 设置边框宽度 */
    	const borderWidth = 2;
        /** 开始一条新路径 */
        ctx.beginPath();
        /** 设置路径起点 */
        ctx.moveTo(radius, radius);
        /** 填充颜色 */ 
        ctx.fillStyle = color;
        /** 绘制扇形 (圆心坐标,圆心坐标,半径,扇形起始角度,扇形终止角度) */
        ctx.arc(radius, radius, radius, (270 - degree  + (degree * i)) * PI / 180, (270 - degree  + degree + (degree * i)) * PI / 180);
        /** 自动绘制一条当前点到起点的直线,形成一个封闭图形,省却使用一次moveTo方法。 */
        ctx.closePath();
        /** 闭合路径 */
        ctx.fill();
        /** 绘制边框 */
    	ctx.lineWidth = borderWidth;
    	ctx.strokeStyle = borderColor;
    	ctx.stroke();
      }
    }
  },
}
</script>

<style lang="less">

</style>

二、最后效果

在这里插入图片描述

基于VUE3转盘组件的开发

该版本与vue2版本的差别在该版本于基于vue3进行开发,并且绘制扇形规则不同,该版本增加扇形border不会给圆弧增加border,而是在扇形的左右增加border,并增加了弹幕播报。额外注意的是,vue3不支持行内使用widow对象。

1.H5布局

<template>
  <div :class="device" ref="container">
    <!-- 导航 -->
    <!-- <VantNav /> -->
    <!-- 内容 -->
    <van-row class="container">
      <van-col span="24" class="turntableBox">
        <van-barrage class="barrage" v-model="barrageList" :auto-play="true" :duration="8000" :delay="1000">
        </van-barrage>
        <div class="turntableMain" :style="`height:${windowWidth * 0.8}px;width:${windowWidth * 0.8}px;`">
          <div ref="oTurntable" class="turntable"
            :style="`height:${windowWidth * 0.8}px;width:${windowWidth * 0.8}px;`">
            <div>
              <Canvas :windowWidth="windowWidth" :prizeTotal="activeInfo.prizeList.length" />
            </div>
            <div class="prizeBox">
              <div class="prizeItem"
                :style="`width:${perPrize.width}px;height:${perPrize.height}px;transform:translateX(-50%) rotate(-${(perPrize.degree * (index + 1)) - (perPrize.degree / 2)}deg);left:calc(50%)`"
                v-for="(item, index) in activeInfo.prizeList" :key="index">
                <p class="title">{{ item.name }}</p>
                <!-- <p class="describe">{{ item.describe }}</p> -->
                <img :src="item.img"/>
              </div>
            </div>
          </div>
          <van-image class="go" fit="cover" width="42px" :src="goPointer" @click="go" />
        </div>
      </van-col>
      <van-col span="24">
        <div id="result"></div>
      </van-col>
    </van-row>
  </div>
</template>

<script setup>
import Canvas from "./myCanvas.vue";
import { activityQuestions, verifyLottery, verifyWin, failAnswer } from "@/api/index";
import { getAllParams } from "@/utils/tools";
import Highlight from "@/assets/images/Highlight.png";
import gift from "@/assets/images/gift.png";
import goPointer from "@/assets/images/goPointer.png";
import { ref, reactive, onMounted, onBeforeUnmount, computed } from 'vue';
import { useRouter } from 'vue-router';
import { showFailToast, showToast } from 'vant';
/** url携带参数 */
const router = useRouter();
const params = reactive(router.currentRoute.value.query);
const windowWidth = ref(0);
const isUrl = /^(http|https):\/\/[^ " ]+$/;
const device = window.localStorage.getItem('device');
let container = ref();
/** 活动设置 */
const activeInfo = reactive({
  /** 中奖概率 */
  probabilities: {
    "一等奖": 10,
    "二等奖": 10,
    "三等奖": 10,
    "四等奖": 10,
  },
  /** 奖品信息 */
  prizeList: [
    {
      name: '一等奖',
      describe: '一等奖',
      img: 'https://img01.yzcdn.cn/vant/cat.jpeg'
    },
    {
      name: '未中奖',
      describe: '未中奖',
      img: 'https://img01.yzcdn.cn/vant/cat.jpeg'
    },
    {
      name: '二等奖',
      describe: '二等奖',
      img: 'https://img01.yzcdn.cn/vant/cat.jpeg'
    },
    {
      name: '未中奖',
      describe: '未中奖',
      img: 'https://img01.yzcdn.cn/vant/cat.jpeg'
    },
    {
      name: '三等奖',
      describe: '三等奖',
      img: 'https://img01.yzcdn.cn/vant/cat.jpeg'
    },
    {
      name: '四等奖',
      describe: '四等奖',
      img: 'https://img01.yzcdn.cn/vant/cat.jpeg'
    },

  ]
});

/** 是否正在执行动画 */
let isGo = ref(false);
/** 执行动画的对象 */
let oTurntable = ref();
/** 即将旋转的度数 */
let randomDeg = ref(0);
/** 上一次旋转的度数 */
let lastDeg = ref(0);
/** 抽奖次数 */
let goTimes = ref(3);
/** 奖品图片 */
let perPrize = reactive({
  degree: null,
  width: null,
  height: null
});

/** 弹幕 */
let defaultList = ref([
  { text: '轻量' },
  { text: '可定制的' },
  { text: '移动端' },
  { text: 'Vue' },
  { text: '组件库' },
  { text: 'VantUI' },
  { text: '666' },
]);
const barrageList = ref([...defaultList.value]);

onMounted(() => {
  windowWidth.value = window.innerWidth;
  /** 奖品 */
  const angle = (360 / activeInfo.prizeList.length) / 2; // 对角角度
  const ratio = Number(Math.sin(angle * (Math.PI * 2 / 360)).toFixed(2)); // 与半径的比率
  perPrize = {
    degree: (360 / activeInfo.prizeList.length),
    width: Math.floor((windowWidth.value * ratio)) / 2,
    /** 高度是直径的一半 */
    height: windowWidth.value * 0.8 / 2
  }
});

/** 点击抽奖 */
const go = () => {
  /** 正在抽奖,未结束继续点击无效 */
  if (!isGo.value && goTimes.value > 0) {
    const result = generatePrize();
    /** 
     * 获取奖项下标
     * 奖项名字可能会重复,所以需要找到奖项的所有下标保存到数组里
     * 根据下标数组随机生成一个数字来决定选择哪个下标成为最终结果的下标
     *  */
    const resultIndexArray = activeInfo.prizeList.reduce((acc, item, index) => {
      if (item.name === result) {
        acc.push(index);
      }
      return acc;
    }, []);
    const randomResultIndex = Math.floor(Math.random() * resultIndexArray.length);
    const index = resultIndexArray[randomResultIndex];
    /** 奖项总和数量 */
    const length = activeInfo.prizeList.length;
    ratating((360 / length * index) + (360 / length / 2), result);
  }
  else if (!isGo.value && goTimes.value <= 0) {
    showToast('抱歉,您的抽奖次数用完了');
  }
  else {
    showToast('请勿重复点击');
    return
  }
};

/** 获取抽奖结果 */
const generatePrize = () => {
  /** 生成一个 0 到 99 之间的随机数 */
  const randomNum = Math.floor(Math.random() * 100);
  let cumulativeProbability = 0;
  for (const prize in activeInfo.probabilities) {
    cumulativeProbability += activeInfo.probabilities[prize];
    if (randomNum < cumulativeProbability) {
      /** 返回中奖内容 */
      return prize;
    }
  }
  // 默认返回未中奖
  return "未中奖";
};

/** 该方法能产生[n,m]之间随机数,决定转盘转多少圈 */
const getRandom = (n, m) => {
  let result = Math.floor(Math.floor(Math.random() * (m - n + 1) + n))
  return result;
};

/** 旋转 */
const ratating = (deg, text) => {
  goTimes.value--;
  isGo.value = true;
  /** 旋转圈数 */
  let turnNumber = getRandom(3, 6);
  /** 记录这次要旋转的度数(传来的度数+圈数) */
  randomDeg.value = deg + 360 * turnNumber;
  /*上次指针离初始状态的度数 + 上次的度数 + 这次要旋转的度数
  (这样的目的是为了每次旋转都从原点开始,保证数据准确)*/
  let realDeg = (360 - lastDeg.value % 360) + lastDeg.value + randomDeg.value;
  /** 为对象添加执行动画 */
  oTurntable.value.style.transform = `rotate(${realDeg}deg)`;
  setTimeout(() => {
    isGo.value = false;
    var list = document.getElementById('result');
    list.innerHTML += /未中奖/.test(text) ? `<p>很遗憾,您${text}!</p>` : `<p>恭喜您,获得${text}!</p>`;
    barrageList.value.push({ text: /未中奖/.test(text) ? `很遗憾,您${text}` : `恭喜您,获得${text}` });
    /** 把这次度数存储起来,方便下一次获取 */
    lastDeg.value = realDeg;
  }, 4000);
};

</script>

<style lang="less" scoped>
::v-deep {
  .van-barrage__item {
    text-shadow: none;
    color: #fff;
    display: inline-block;
    background-color: rgba(0, 0, 0, .5);
    height: 28px;
    border-radius: 28px;
    line-height: 28px;
    padding: 0 20px;
    margin-bottom: 12px;
    font-size: 14px;
  }
}

.container {
  min-height: 100vh;
  width: 100%;
  overflow: hidden;
}

.barrage {
  height: 150px;
  width: 100%;
}

.turntableBox {

  .turntableMain {
    margin: 0 auto;
    position: relative;
    border: 10px solid #E5E5E5;
    border-radius: 100%;
  }

  .turntable {
    transition: transform 4s;
    margin: 0 auto;
  }

  .go {
    position: absolute;
    top: calc(50% - 31px);
    left: calc(50% - 21px);
  }

  .prizeBox {
    position: absolute;
    width: 80%;
    top: 0;
    left: calc(50% - 40%);

    .prizeItem {
      display: flex;
      justify-content: center;
      flex-direction: column;
      align-items: center;
      text-align: center;
      position: absolute;
      top: 0;
      overflow: hidden;
      text-align: center;
      transform-origin: center bottom;
      transform: translateX(-50%);
      color: #2c3e50;

      p {
        margin: 0;
        padding: 0;
      }

      .title {
        font-size: 18px;
        margin-top: 12px;
      }

      .describe {
        font-size: 14px;
        line-height: 28px;
        white-space: break-spaces;
      }

      img {
        margin-top: 16px;
        margin-bottom: 58px;
        width: 50%;
      }
    }
  }
}
</style>

2.饼图绘制

<template>

  <canvas class="canvas" id="canvasImg" :style="`width:${perimeter}px;height: ${perimeter}px;`">您的浏览器不支持canvas!</canvas>

</template>

<script setup>
import { ref, reactive, onMounted, onBeforeUnmount, defineProps } from 'vue';
/** 直径 */
let perimeter = ref(0);
let container = ref();

const props = defineProps({
  prizeTotal: Number,

});


onMounted(() => {
  perimeter.value = window.innerWidth * 0.8;
  drawPie();

});

/** 画饼图 */
const drawPie = () => {
  const PI = Math.PI;
  /** 获取画布并获取2d上下文对象 */
  const canvas = document.getElementById('canvasImg');
  const ctx = canvas.getContext('2d');
  /** 假设周长为500 */
  const perimeterValue = perimeter.value;
  /** 半径 */
  const radius = perimeterValue * 0.5;
  /** 总奖品数 */
  const prizeTotal = props.prizeTotal;
  /** 每个扇形的角度=360度 / 总奖品数 */
  const degree = 360 / prizeTotal;
  /** 画布宽高 */
  canvas.width = perimeterValue;
  canvas.height = perimeterValue;
  /** 根据奖品数把圆形分成等份的扇形 */
  for (let i = 0; i < prizeTotal; i++) {
    /** 奇偶颜色 */
    const fillColor = i % 2 === 0 ? "#F8D383" : "#F8E2BC";
    /** 设置边框颜色 */
    const borderColor = "#fff";
    /** 设置边框宽度 */
    const borderWidth = 0.5; 

    /** 计算扇形起始角度和终止角度 */
    const startAngle = (270 - degree + (degree * i)) * PI / 180;
    const endAngle = (270 - degree + degree + (degree * i)) * PI / 180;

    /** 开始一条新路径 */
    ctx.beginPath();
    /** 设置路径起点 */
    ctx.moveTo(radius, radius);
    /** 填充颜色 */
    ctx.fillStyle = fillColor;
    /** 绘制扇形 */
    ctx.arc(radius, radius, radius, startAngle, endAngle);
    /** 自动绘制一条当前点到起点的直线,形成一个封闭图形 */
    ctx.closePath();
    /** 填充扇形 */
    ctx.fill();

    /** 绘制边框的左右两个边 */
    ctx.beginPath();
    ctx.lineWidth = borderWidth;
    ctx.strokeStyle = borderColor;

    /** 计算起始边框的坐标 */
    const x1 = radius + radius * Math.cos(startAngle);
    const y1 = radius + radius * Math.sin(startAngle);
    ctx.moveTo(radius, radius);
    ctx.lineTo(x1, y1);

    /** 计算结束边框的坐标 */ 
    const x2 = radius + radius * Math.cos(endAngle);
    const y2 = radius + radius * Math.sin(endAngle);
    ctx.moveTo(radius, radius);
    ctx.lineTo(x2, y2);

    ctx.stroke();
  }
}

</script>

<style lang="less" scoped>
.canvas {
  margin: 0 auto;
}
</style>

二、最后效果

在这里插入图片描述


总结

本文仅仅简单记录了转盘组件的基本实现,仅供学习参考。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值