最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • Vue3+TS+Vite2+Element Plus 两天写个抽奖小系统(前端篇)

    正文概述 掘金(konidexin)   2021-03-30   1086

    功能介绍

    功能清单

    功能不多,毕竟是随意练手的,目前给的是个本地版的,不需后台就能使用的。

    主要功能:

    • 抽奖逻辑
    • 奖品清单
    • 中奖名单
    • 奖品数量控制
    • 用户操作提示
    • 背景音乐控制

    界面预览

    抽奖页面

    Vue3+TS+Vite2+Element Plus 两天写个抽奖小系统(前端篇)

    奖品清单

    Vue3+TS+Vite2+Element Plus 两天写个抽奖小系统(前端篇)

    中奖名单

    Vue3+TS+Vite2+Element Plus 两天写个抽奖小系统(前端篇)

    撸代码前准备

    看标题大家应该就清楚用的技术栈是什么了,没啥好说的,前端攻城狮就是得用新技术哈~然后我用的编辑器是VSCode。

    Vite创建项目

    如果不知道Vite是啥?那你先去官网cn.vitejs.dev/ 了解一下。

    yarn create @vitejs/app
    

    命令行输完,会有一个交互操作,这里你输入自己的项目名称,模板使用vue3+ts。

    ESLint和Prettier配置

    安装依赖

    相关的包有:

    # 这里为了让大家看的清楚一些,所以分开安装
    
    yarn add -D prettier
    yarn add -D eslint
    yarn add -D eslint-plugin-vue
    yarn add -D eslint-plugin-prettier
    yarn add -D eslint-config-prettier
    yarn add -D @typescript-eslint/eslint-plugin
    yarn add -D @typescript-eslint/parser
    

    ESLint配置

    在项目根目录创建.eslintrc.js文件,配置如下(个人喜好不同,规则rules可以自己修改):

    module.exports = {
      parser: 'vue-eslint-parser',
      parserOptions: {
        parser: '@typescript-eslint/parser',
        ecmaVersion: 2020,
        sourceType: 'module'
      },
      extends: [
        'plugin:vue/vue3-recommended',
        'plugin:@typescript-eslint/recommended',
        'plugin:prettier/recommended',
        'prettier'
      ],
      rules: {
        'no-unused-vars': 'off',
        '@typescript-eslint/no-unused-vars': 'off',
        '@typescript-eslint/explicit-module-boundary-types': 'off'
      }
    }
    

    Prettier配置

    在项目根目录创建prettier.config.js文件,配置如下(根据自己编码习惯自行调整):

    // 看单词应该就懂什么意思了,还是不太懂的朋友可以百度
    module.exports = {
      printWidth: 80,
      tabWidth: 2,
      useTabs: false,
      vueIndentScriptAndStyle: false,
      singleQuote: true,
      quoteProps: 'as-needed',
      trailingComma: 'none',
      endOfLine: 'auto',
      semi: false
    }
    

    编辑器配置(可选)

    因为我自己会经常在MacOS和Windows切换办公,所以配置一下编辑器保持统一,配置前需要在VSCode插件商店安装EditorConfig for VS Code插件。

    安装完后在项目根目录创建.editorconfig文件,配置如下:

    # 编辑器配置
    
    root = true
    
    [*]
    charset = utf-8
    end_of_line = lf
    indent_size = 2
    indent_style = space
    # insert_final_newline = true
    trim_trailing_whitespace = true
    
    [*.md]
    trim_trailing_whitespace = false
    

    Element Plus配置

    组件按需引用我用的是vite-plugin-imp插件,官方文档用的是vite-plugin-style-import,这边你自己选择。

    方式一:参考官方文档:Element+快速开始

    方式二:

    yarn add element-plus vite-plugin-imp
    

    安装后打开根目录vite.config.ts文件配置:

    import { defineConfig } from 'vite'
    import vue from '@vitejs/plugin-vue'
    import vitePluginImp from 'vite-plugin-imp'
    import path from 'path'
    
    // https://vitejs.dev/config/
    export default defineConfig({
      resolve: {
        alias: {
          '@/': `${path.resolve(__dirname, 'src')}/`
        }
      },
      plugins: [
        vue(),
        vitePluginImp({
          libList: [
            {
              libName: 'element-plus',
              style: (name) => {
                return `element-plus/lib/theme-chalk/${name}.css`
              }
            }
          ]
        })
      ]
    })
    

    如果引入path有报错,需要安装一下@types/node:

    yarn add -D @types/node
    

    Vue Router和Vuex

    这边项目功能比较简单,就不作配置了,想要了解的同学直接去官网文档一看就知道,或者用vue-cli创建一个vue3的项目看一下代码就行了。

    准备就绪,开撸代码

    template部分

    <template>
      <div>
        <!-- 音频相关 -->
        <audio ref="music" preload="auto" loop :src="musicFile" />
        <audio ref="bgm" preload="auto" loop :src="bgmFile" />
    
        <!-- 背景装饰图片 -->
        <div class="heng-fu">
          <el-image :src="hengFuImg" />
        </div>
        <div class="deng-long-left">
          <el-image :src="dengLongImg" />
        </div>
        <div class="deng-long-right">
          <el-image :src="dengLongImg" />
        </div>
        <div class="niu-left">
          <el-image :src="niuLeftImg" />
        </div>
        <div class="niu-right">
          <el-image :src="niuRightImg" />
        </div>
    
        <!-- 按钮相关 -->
        <div class="music-btn">
          <input
            type="image"
            :src="musicImg"
            
            class="btn-music"
            @click="musicOpen = !musicOpen"
          />
        </div>
        <el-tooltip content="抽奖" placement="top" effect="light">
          <input
            type="image"
            :src="jinLiImg"
            class="btn-jin-li"
            @click="lotteryBtnClick"
          />
        </el-tooltip>
        <el-tooltip content="奖品" placement="top" effect="light">
          <input
            type="image"
            :src="hongBaoImg"
            class="btn-hong-bao"
            @click="prizeDrawer = true"
          />
        </el-tooltip>
        <el-tooltip content="中奖名单" placement="top" effect="light">
          <input
            type="image"
            :src="huaDuoImg"
            class="btn-hua-duo"
            @click="recordDrawer = true"
          />
        </el-tooltip>
    
        <!-- 中间头像相关 -->
        <div class="avatar">
          <el-avatar
            :size="200"
            :src="avatarUrl"
            shape="square"
            class="avatar-border"
            @click="retryLottery"
          />
          <div class="name-label">{{ currentName }}</div>
        </div>
    
        <!-- 奖品清单侧边栏 -->
        <el-drawer v-model="prizeDrawer"  size="400">
          <div style="padding: 0 20px 20px 20px">
            <el-select v-model="currentPrizeTitle" placeholder="请选择抽取的奖品">
              <el-option
                v-for="prize in prizes"
                :key="prize.type"
                :label="prize.title"
                :value="prize.title"
                :disabled="isPrizeUnavailable(prize.title)"
              >
                <span style="float: left">{{ prize.title }}</span>
                <span style="float: right; color: #8492a6; font-size: 13px">{{
                  prize.text
                }}</span>
              </el-option>
            </el-select>
          </div>
          <el-space direction="vertical">
            <el-card v-for="prize in prizes" :key="prize.type" class="prize-card">
              <el-image
                style="width: 100px; height: 100px"
                :src="prize.img"
                fit="contain"
              />
              <div style="margin-left: 20px">
                <h4>{{ prize.text }} / {{ prize.count }}个</h4>
                <h5>{{ prize.title }}</h5>
              </div>
            </el-card>
          </el-space>
        </el-drawer>
    
        <!-- 中奖名单侧边栏 -->
        <el-drawer v-model="recordDrawer"  size="400">
          <el-empty v-if="lotteryRecords.length <= 0" description="暂无记录" />
          <el-space v-else direction="vertical" :size="0">
            <div
              v-for="(record, index) in lotteryRecords"
              :key="index"
              class="lottery-record"
            >
              <div class="lottery-record-content">
                <el-avatar :size="50" :src="record.avatar" />
                <div style="margin-left: 20px">
                  <span style="font-weight: bold">{{ record.name }}</span> 抽中
                  <span style="font-weight: bold">{{ record.prize }}</span>
                </div>
              </div>
              <el-divider style="margin: 10px 0" />
            </div>
          </el-space>
        </el-drawer>
      </div>
    </template>
    

    style部分

    <style>
    body {
      margin: 0;
    }
    
    #app {
      width: 100vw;
      height: 100vh;
      background-color: #f39f86;
      background-image: linear-gradient(315deg, #f39f86 0%, #f9d976 74%);
    }
    
    .heng-fu {
      position: absolute;
      top: 24px;
      left: 50%;
      transform: translateX(-50%);
    }
    
    .deng-long-left {
      position: absolute;
      left: 30px;
      top: 0;
    }
    
    .deng-long-right {
      position: absolute;
      right: 30px;
      top: 0;
    }
    
    .niu-left {
      position: absolute;
      bottom: 0;
      left: 30px;
    }
    
    .niu-right {
      position: absolute;
      bottom: 0;
      right: 30px;
    }
    
    .btn-jin-li {
      position: absolute;
      bottom: 55px;
      left: calc(50% - 250px);
      width: 130px;
      height: 60px;
    }
    
    .btn-hong-bao {
      position: absolute;
      bottom: 30px;
      left: 50%;
      width: 130px;
      height: 133px;
      transform: translateX(-50%);
    }
    
    .btn-hua-duo {
      position: absolute;
      bottom: 0;
      left: calc(50% + 120px);
      width: 85px;
      height: 130px;
    }
    
    .music-btn {
      position: absolute;
      right: 15px;
      top: 15px;
    }
    
    .btn-music {
      width: 40px;
      height: 40px;
    }
    
    .avatar {
      position: absolute;
      left: 50%;
      top: 50%;
      transform: translateX(-50%) translateY(-50%);
    }
    
    .avatar-border {
      padding: 5px;
      background: white;
    }
    
    .name-label {
      margin-top: 10px;
      font-size: 30px;
      color: #fff;
      font-weight: bold;
      text-align: center;
    }
    
    input:focus {
      outline: none;
    }
    
    .prize-card {
      margin: 0 20px;
      width: 360px;
    }
    
    .el-card__body {
      padding: 0;
      height: 100px;
      display: flex;
      flex-direction: row;
    }
    
    .el-drawer {
      width: 400px;
      overflow: auto;
      display: flex;
      flex-direction: column;
    }
    
    .lottery-record {
      margin: 0 20px;
      width: 360px;
    }
    
    .lottery-record-content {
      display: flex;
      flex-direction: row;
      align-items: center;
    }
    </style>
    

    script部分

    import { computed, defineComponent, ref, watch } from 'vue'
    import { ElMessage, ElNotification, ElMessageBox } from 'element-plus'
    import personJsonData from './assets/json/person.json'
    import { prizes } from './models/prize'
    import { res } from './composables/res'
    import {
      musicOpen,
      switchLotteryEffect,
      music,
      bgm
    } from './composables/music-control'
    
    export default defineComponent({
      name: 'App',
      setup() {
        /** 是否打开奖品侧边栏 */
        const prizeDrawer = ref(false)
        /** 中奖名单侧边栏是否打开 */
        const recordDrawer = ref(false)
        /** 抽奖是否结束 */
        const lotteryFinish = ref(false)
        // 所有人员名单
        const personList = ref<Person[]>(personJsonData)
        // 当前人员头像
        const avatarUrl = ref(personList.value[0].avatar)
        // 当前人员姓名
        const currentName = ref(personList.value[0].name)
    
        const availablePersons = computed(() => {
          // 设置当前可以获奖的人员名单,移除已经获奖的
          const personToRemove = lotteryRecords.value.map((item) => item.name)
          const filterArr = personList.value.filter(
            (item) => personToRemove.indexOf(item.name) < 0
          )
          return filterArr
        })
    
        // 中奖记录
        const lotteryRecords = ref<LotteryRecord[]>([])
    
        // 监听中奖记录,处理奖品的切换和抽奖是否结束状态
        watch(
          lotteryRecords,
          () => {
            if (lotteryRecords.value.length > 0) {
              // 判断奖品是否抽完以及自动切换奖品
              const isCurrentPrizeUnavailable = isPrizeUnavailable(
                currentPrizeTitle.value
              )
              if (isCurrentPrizeUnavailable) {
                const prePrize = currentPrizeTitle.value
                if (currentPrizeIndex.value > 0) {
                  currentPrizeIndex.value--
                  ElNotification({
                    title: '消息提醒',
                    message: `奖品 【${prePrize}】 已经抽完,开始抽取奖品 【${
                      prizes[currentPrizeIndex.value].title
                    }】`,
                    position: 'top-left',
                    type: 'info'
                  })
                } else {
                  lotteryFinish.value = true
                  ElNotification({
                    title: '消息提醒',
                    message: '奖品全部抽完,抽奖已经结束',
                    position: 'top-left',
                    type: 'warning'
                  })
                }
              }
            }
          },
          { deep: true }
        )
    
        // 设置可抽奖人数的随机最大index
        const maxIndex = computed(() => availablePersons.value.length - 1)
    
        // 是否正在抽奖
        const lotteryRunning = ref(false)
        watch(lotteryRunning, () => {
          switchLotteryEffect(lotteryRunning.value)
        })
    
        // 抽奖定时器
        let timer: NodeJS.Timeout
    
        // 抽奖
        const lottery = () => {
          lotteryRunning.value = true
          const index = Math.round(Math.random() * maxIndex.value)
          avatarUrl.value = availablePersons.value[index].avatar
          currentName.value = availablePersons.value[index].name
          timer = setTimeout(lottery, 50)
        }
    
        // 停止抽奖
        const stopLottery = () => {
          lotteryRunning.value = false
          clearTimeout(timer)
    
          const record: LotteryRecord = {
            avatar: avatarUrl.value,
            name: currentName.value,
            prize: currentPrizeTitle.value,
            prizeIndex: currentPrizeIndex.value
          }
          lotteryRecords.value.unshift(record)
        }
    
        // 监听是否正在抽奖
        watch(lotteryRunning, () => {
          if (lotteryRunning.value) {
            ElMessage.success({
              message: `正在抽取 ${currentPrize.value.text} / ${currentPrize.value.title}`,
              type: 'success'
            })
    
            lottery()
          } else {
            stopLottery()
          }
        })
    
        /**
         * 抽奖按钮点击
         */
        const lotteryBtnClick = () => {
          if (lotteryFinish.value) {
            ElNotification({
              title: '消息提醒',
              message: '奖品全部抽完,抽奖已经结束',
              position: 'top-left',
              type: 'warning'
            })
          } else {
            lotteryRunning.value = !lotteryRunning.value
          }
        }
    
        /**
         * 头像点击重新抽奖
         */
        const retryLottery = () => {
          console.log('retryLottery')
          if (lotteryRecords.value.length <= 0) return
    
          ElMessageBox.confirm('此操作将重新抽取刚才的奖品,是否继续?', '提示', {
            confirmButtonText: '确定',
            cancelButtonText: '取消',
            type: 'warning'
          })
            .then(() => {
              const recordToRemove = lotteryRecords.value[0]
              if (recordToRemove.prizeIndex !== currentPrizeIndex.value) {
                currentPrizeIndex.value = recordToRemove.prizeIndex
              }
    
              lotteryRecords.value.splice(0, 1)
              ElMessage.success({
                message: '操作成功,请继续进行抽奖',
                type: 'success'
              })
            })
            .catch(() => {
              console.log('cancel')
            })
        }
    
        /** 当前奖品的对应数组的index */
        const currentPrizeIndex = ref(prizes.length - 1)
    
        /** 监听当前奖品的index */
        watch(
          currentPrizeIndex,
          () => (currentPrizeTitle.value = prizes[currentPrizeIndex.value].title)
        )
    
        /** 当前奖品的名称 */
        const currentPrizeTitle = ref(prizes[currentPrizeIndex.value].title)
    
        /** 当前正在抽的奖品 */
        const currentPrize = computed(() => {
          const filterArr = prizes.filter(
            (item) => item.title === currentPrizeTitle.value
          )
          return filterArr.length > 0 ? filterArr[0] : <Prize>{}
        })
    
        /** 判断奖品是否抽满 */
        const isPrizeUnavailable = (prizeTitle: string) => {
          const filterPrizes = prizes.filter((item) => item.title === prizeTitle)
          if (filterPrizes.length <= 0) return false
    
          const prize = filterPrizes[0]
          const filterRecords = lotteryRecords.value.filter(
            (item) => item.prize === prizeTitle
          )
          return prize.count <= filterRecords.length
        }
    
        return {
          ...res,
          avatarUrl,
          currentName,
          lotteryBtnClick,
          retryLottery,
          prizeDrawer,
          prizes,
          currentPrizeTitle,
          isPrizeUnavailable,
          lotteryRecords,
          recordDrawer,
          musicOpen,
          music,
          bgm
        }
      }
    })
    

    前端源码

    GitHub地址 Gitee

    后端管理系统+API接口

    毕竟随手写写的项目,花了两小时用Django+Django Rest Framework写了一个配套的后端部分。

    API接口界面

    Vue3+TS+Vite2+Element Plus 两天写个抽奖小系统(前端篇)

    管理系统界面

    Vue3+TS+Vite2+Element Plus 两天写个抽奖小系统(前端篇)

    这里就不多讲了,有兴趣的朋友可以直接看代码~

    后端源码

    Gitee


    起源地下载网 » Vue3+TS+Vite2+Element Plus 两天写个抽奖小系统(前端篇)

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元