功能介绍
功能清单
功能不多,毕竟是随意练手的,目前给的是个本地版的,不需后台就能使用的。
主要功能:
- 抽奖逻辑
- 奖品清单
- 中奖名单
- 奖品数量控制
- 用户操作提示
- 背景音乐控制
界面预览
抽奖页面
奖品清单
中奖名单
撸代码前准备
看标题大家应该就清楚用的技术栈是什么了,没啥好说的,前端攻城狮就是得用新技术哈~然后我用的编辑器是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接口界面
管理系统界面
这里就不多讲了,有兴趣的朋友可以直接看代码~
后端源码
Gitee
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!