1. 説明
- これは、vue3 + Ts + Vite + ElementPlus によって実装された宝くじプログラムです。
- プロジェクトリンク
2.全体構造と機能説明
左侧设置了奖品说明,每个奖项配有文字和图片简介
。特別賞1名、1等2名、2等5名、3等10名の計4賞を設定する。(現在、各賞の順位は固定値に設定されていますが、今後はユーザーが柔軟に設定できるように調整可能な値に設定することを検討しています。)中间部分设置四个奖项的切换按钮,和“开始”抽奖的按钮。
「開始」をクリックすると抽選参加者の名前が画面上でランダムにスクロールし、再度クリックすると当選者の名前がポップアップ表示され、同時に名前が保存されます。各賞の枠がいっぱいになると「開始」ボタンがグレー表示になり、くじをクリックすると「この賞は抽選されました」というポップアップが表示されます。右侧设置了三个按钮,分别是:参与人员、抽奖记录、重新抽奖。
- 参加者: ダウンロード テンプレート、クリア データを提供し、ユーザーはデータをカスタマイズしてインポートできます。
- 抽選記録: 当選者、部門、賞のリストを表示します。
- 再抽選: 前回の抽選記録をリセットします。最大までリセットできます。
说明:目前的设置是每个人只能有一次中奖机会,即中奖后不能在中奖另一种奖项。
3. データストレージ
无后台,纯前端实现
さらに、データを失わずにブラウザを更新して閉じる必要があります。localStorage を使用すると、localStorage に保存されたデータは永続的であり、ブラウザを更新したり閉じたりしても (手動で意図的に消去しない限り) 変更されません。
4. 主な機能説明
- アワードスイッチ:
<el-radio-group v-model="prizeValue" class="radioGroup" @change="changePrizeType">
<el-radio-button v-for="item in prizeType" :label="item" :key=item />
</el-radio-group>
const prizeType = ['特等奖','一等奖','二等奖','三等奖']
const prizeValue = ref<string>('特等奖')
// 改变抽奖类型
const changePrizeType = (type: string) => {
prizeValue.value = type
// 将按钮文字重置为开始
showName.value = '开始'
if(haveRemeber.value){
// 校验当前奖项是否已经被抽完,第一次抽奖时候不校验
const luckNameList = JSON.parse(localStorage.getItem('luckNameList')!)
if(luckNameList && luckNameList.length){
checkoutDraw(luckNameList)
}
}
}
- 名前ロールと一時停止:
// 1. 洗牌算法:用于姓名每次循环前获得一个随机的姓名数组。主要目的是确保抽奖的公平性即每个人中奖的概率都相同
const shuffle = (arr: any) => {
const arrNew = []; // 打乱后的数组
const len=arr.length
for (let i=len;i>0;i--){
// 生成一个在0-len之间的随机数
const rand=Math.floor(Math.random()*i)
// 从原数组中拿出这个随机下标对应的数放入新数组当中
arrNew.push(arr[rand]);
// 从原数组当中删除拿出的这个值
arr.splice(rand,1)
}
return arrNew;
}
// 2. 循环姓名列表,每次到最后一个姓名结束时,重新在循环一遍,如此往复。
const forNameList = (list: any) => {
list = shuffle(list);
for(let i=0; i<list.length; i++){
setTimeout(() => {
if(!isStop.value){
showName.value = list[i].name;
if(i == list.length - 1){
// 当数组循环结束后,没有停止就在继续循环
// 获取所有的姓名列表
const allNameList = JSON.parse(localStorage.getItem('nameList')!)
// 获取中奖人员姓名列表
const luckNameList = JSON.parse(localStorage.getItem('luckNameList')!)
if(luckNameList){
// 将中奖人员从下一次抽奖中过滤掉,确保同一奖项每个人只能有一次中奖机会
const newNameList = allNameList.filter((itemA: any) => luckNameList.every((itemB: any) => itemB.name !== itemA.name))
// 将新的姓名列表重新赋值给 useNameList
useNameList.value = newNameList
}else{
useNameList.value = allNameList
}
forNameList(useNameList.value)
}
}
},50 * i);
}
}
// 3. 开始抽奖与暂停
const drawStart = () => {
// isStop.value ? startDraw() : stopDraw()
const luckNameList = JSON.parse(localStorage.getItem('luckNameList')!)
if(haveRemeber.value){
// 如果没有参与人员不进行后续操作直接弹出导入人员提示
if(luckNameList && luckNameList.length){
// 校验当前奖项是否已经被抽完
const flag = checkoutDraw(luckNameList)
// 根据 startStatus 的状态决定当前奖项能否再抽
if(flag){
if(isStop.value){
startDraw()
}else{
stopDraw()
}
}else{
// 不可以抽奖,说明该奖项名额已经抽完了
ElMessage.warning(`${
prizeValue.value}` + '已经抽完了!')
}
}else{
//此时没有中奖人员,任何奖项下都可以抽奖
if(isStop.value){
startDraw()
}else{
stopDraw()
}
}
}else{
dialogVisible.value = true
}
}
// 开始
const startDraw = () => {
// 如果没有导入抽奖人员数据则提示 “请先导入抽奖人员数据!”
if(!haveRemeber.value){
dialogVisible.value = true
}else{
// 如果有数据开始循环滚动姓名,再次点击则停止滚动并弹出中奖人员
isStop.value = false // 开始循环
// 获取所有的姓名列表
const allNameList = JSON.parse(localStorage.getItem('nameList')!)
// 获取中奖人员
const luckNameList = JSON.parse(localStorage.getItem('luckNameList')!)
// 如果清除了中奖人员
if(!luckNameList || luckNameList && !luckNameList.length){
useNameList.value = allNameList
}
forNameList(useNameList.value) // 循环姓名数组
}
}
// 暂停
const stopDraw = () => {
isStop.value = true
dialogVisible.value = true
// 获取中奖人员数据弹窗显示,并存储起来
const tableData = JSON.parse(localStorage.getItem('tableData')!) // 所有数据
let tableDrawData: any = JSON.parse(localStorage.getItem('luckNameList')!) || []
tableData.forEach((item: any) => {
if(item.name == showName.value){
tableDrawData.push({
name: item.name,
sex: item.sex,
dept: item.dept,
prize: prizeValue.value
})
}
})
localStorage.setItem('luckNameList',JSON.stringify(tableDrawData))
// 获取所有的姓名列表
const allNameList = JSON.parse(localStorage.getItem('nameList')!)
// 将中奖人员从下一次抽奖中过滤掉,确保每个人只能有一次中奖机会,也就是从 allNameList 中过滤掉 tableDrawData中的元素
const newNameList = allNameList.filter((itemA: any) => tableDrawData.every((itemB: any) => itemB.name !== itemA.name))
// 将新的姓名列表重新赋值给 useNameList
useNameList.value = newNameList
// 校验当前奖项是否已经被抽完
checkoutDraw(tableDrawData)
}
- 現在の特典が抽選されたかどうかの確認、初期化、特典の種類の切り替え、抽選をクリック、抽選終了の確認
const checkoutDraw = (list: luckNameListType[]) => {
// 此时表示有中奖人员,需要根据条件来确认是否可以在继续抽奖
// 整理每一个奖项出现的次数
let prizeList = list.map((item: luckNameListType) => item.prize)
const prizObj = prizeList.reduce((preValue: any, curValue: string)=>{
preValue[curValue] = (preValue[curValue] + 1) || 1
return preValue
},{
})
console.log(prizObj,'prizObj==');
// 如果此时在抽特等奖,只能有 1 个名额
if(prizeValue.value === '特等奖'){
if(prizObj['特等奖'] == 1) {
// 此时特等奖不能在抽了
startStatus.value = false
}else{
startStatus.value = true
}
}else if(prizeValue.value === '一等奖'){
// 如果此时在抽一等奖,只能有 2 个名额
if(prizObj['一等奖'] == 2 ){
// 此时一等奖不能在抽了
startStatus.value = false
} else{
startStatus.value = true
}
}else if(prizeValue.value === '二等奖'){
// 如果此时在抽二等奖,只能有 5 个名额
if(prizObj['二等奖'] == 5){
// 此时二等奖不能在抽了
startStatus.value = false
} else{
startStatus.value = true
}
}else if(prizeValue.value === '三等奖'){
// 如果此时在抽三等奖,只能有 10 个名额
if(prizObj['三等奖'] == 10){
// 此时三等奖不能在抽了
startStatus.value = false
} else{
startStatus.value = true
}
}
return startStatus.value
}
- 再抽選: 前回の宝くじの結果をリセットします。最後までリセットできます。リセット後、宝くじに当選した人が引き続き抽選される名前のリストに追加されます。
const drawAgain = () => {
// 重新抽奖指的是清除最近一次抽奖记录,可以一直重置,直到抽奖记录为空。
let tableDrawData = JSON.parse(localStorage.getItem('luckNameList')!)
if(tableDrawData && tableDrawData.length){
ElMessageBox.confirm(
'确定要重置上一次的抽奖操作吗?',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}
).then(() => {
const popValue = tableDrawData.pop()
localStorage.setItem('luckNameList',JSON.stringify(tableDrawData))
// 重置后刚才中奖的人应该继续放入待抽奖姓名列表中
useNameList.value.push({
name: popValue.name
})
ElMessage.success('重置成功!')
}).catch(() => {
ElMessage.info('取消重置!')
})
}else{
ElMessage.warning('请先完成一次抽奖!')
}
}
- テンプレートのダウンロード: テンプレート ファイルを事前に準備できます。ここのアセットの下に置きます。
import {
saveAs } from 'file-saver'
const downTemplate = () => {
const fileName = '参与抽奖人员模板.xlsx'; // 模板文件名
const fileUrl = './src/assets/template/' // 存放模板文件的路径(相对于index.html)
saveAs(fileUrl + fileName, fileName)
}
- データのインポート
<el-button v-show="drawTitle === '参与人员'" class="importData">
导入数据
<input class="inputFile" type="file" accept=".xls,.xlsx" @change="importData" />
</el-button>
import * as XLSX from 'xlsx'
const importData = (e: any) => {
const file = e.target.files[0] // 获取file对象
const fileReader = new FileReader() // 创建文件读取器
fileReader.onload = (event) => {
const result = event.target!.result // 获取读取的结果
const workBook = XLSX.read(result, {
type: 'binary'}) // xlsx读取返回的结果
const importData = XLSX.utils.sheet_to_json(
workBook.Sheets[workBook.SheetNames[0]]
)
importData.forEach((item: any) => {
tableDataTemp.value.push({
name: item.姓名,
sex: item.性别,
dept: item.部门
})
});
// 只存放姓名
importData.forEach((item: any) => {
tableNameData.value.push({
name: item.姓名
})
});
// 将导入的表格数据存到localStorage中
localStorage.setItem('tableData', JSON.stringify(tableDataTemp.value))
// 将姓名数据存在localStorage中
localStorage.setItem('nameList', JSON.stringify(tableNameData.value))
emits('nameList', tableNameData.value)
// 给当前表格数据赋值
tableData.value = tableDataTemp.value
// 将导入的表格数据中的姓名存到pinia中
appStore.getNameList(tableNameData.value)
}
fileReader.readAsBinaryString(file);
// ((document.getElementsByClassName("inputFile")[0]).value = '')
}
- データのクリア: 参加者のデータと抽選記録をクリアします。これら 2 つは独立しており、相互に影響しません。
const clearData = () => {
ElMessageBox.confirm(
'确定要清空所有数据吗?',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}
).then(() => {
let empty: any = []
tableData.value = []
if(props.drawTitle === '参与人员'){
localStorage.setItem('tableData', JSON.stringify(empty))
localStorage.setItem('nameList', JSON.stringify(empty))
emits('clearData','参与人员')
}else{
localStorage.setItem('luckNameList', JSON.stringify(empty))
emits('clearData','抽奖记录')
}
ElMessage.success('清空成功!')
}).catch(() => {
ElMessage.info('取消清空!')
})
}
5. いくつかの図例
6. 注意事項
後続の関数の追加または変更がある場合、上記のコードは現在の関数のコードの一部のみを表します。上記のコードはそれに応じて変更される可能性があり、変更がある場合は同期的に変更されます。