feat: 🎁 新增破冰抽奖功能及 82 人名单

- 新增 src/views/PrizeDraw 抽奖视图及抽奖配置 store
- 更新 defaultPersonList 为 82 位真实参与者名单
- 调整主页、路由、i18n 及音乐播放以支持抽奖入口
- 附抽奖需求及实现报告文档

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
kkfluous
2026-04-24 16:29:52 +08:00
parent d26c364999
commit 25d0c95dc3
26 changed files with 21649 additions and 281 deletions

View File

@@ -0,0 +1,321 @@
import { defineStore } from 'pinia'
import { getRandomElements } from '@/views/Home/utils/random'
import { defaultPersonList } from './data'
import type { IPersonConfig } from '@/types/storeType'
export interface PrizeConfig {
id: string
name: string
description: string
totalCount: number
remainingCount: number
color: string
order: number
}
export interface DrawResult {
id: string
drawIndex: number
prizeId: string
prizeName: string
prizeDescription: string
drawTime: string
}
export const usePrizeDrawStore = defineStore('prizeDraw', {
state: () => ({
// 配置版本号 - 修改此版本号会强制重新初始化
configVersion: 2,
// 奖品配置
prizeConfigs: [
{
id: 'prize-1',
name: '快乐通勤奖',
description: '可提前1小时下班或晚到1小时',
totalCount: 25,
remainingCount: 25,
color: '#10b981',
order: 1,
},
{
id: 'prize-2',
name: '跑马场自由日',
description: '可选在家或其他场所办公',
totalCount: 18,
remainingCount: 18,
color: '#3b82f6',
order: 2,
},
{
id: 'prize-3',
name: '前途光明奖',
description: '获得boss 1对1畅聊1小时',
totalCount: 2,
remainingCount: 2,
color: '#f59e0b',
order: 3,
},
{
id: 'prize-4',
name: '现金红包',
description: '500元',
totalCount: 3,
remainingCount: 3,
color: '#ef4444',
order: 4,
},
{
id: 'prize-5',
name: '现金红包',
description: '300元',
totalCount: 8,
remainingCount: 8,
color: '#f97316',
order: 5,
},
{
id: 'prize-6',
name: '现金红包',
description: '200元',
totalCount: 15,
remainingCount: 15,
color: '#ec4899',
order: 6,
},
{
id: 'prize-7',
name: '现金红包',
description: '100元',
totalCount: 17,
remainingCount: 17,
color: '#8b5cf6',
order: 7,
},
] as PrizeConfig[],
// 奖品池(用于抽取)
prizePool: [] as string[],
// 人员池
personPool: [] as IPersonConfig[],
// 抽奖结果
drawResults: [] as DrawResult[],
// 当前状态
currentDrawIndex: 0,
isInitialized: false,
}),
getters: {
// 进度百分比
progress: (state) => {
if (state.currentDrawIndex === 0)
return 0
return Math.round((state.currentDrawIndex / 88) * 100)
},
// 剩余人数
remainingPersons: state => state.personPool.length,
// 剩余奖品数
remainingPrizes: state => state.prizePool.length,
// 是否完成
isCompleted: state => state.currentDrawIndex >= 88,
// 总人数
totalPersons: () => 88,
},
actions: {
// 初始化
init() {
// 检查版本号,如果不匹配则强制重新初始化
const expectedVersion = 2
if (this.configVersion !== expectedVersion) {
console.log(`配置版本不匹配 (${this.configVersion} !== ${expectedVersion}),重新初始化...`)
this.reset()
this.configVersion = expectedVersion
}
if (this.isInitialized && this.personPool.length > 0) {
console.log('已初始化,跳过')
return
}
console.log('初始化抽奖系统...')
// 从 data.ts 加载88人
this.personPool = defaultPersonList.map((p, index) => ({
...p,
uuid: `uuid-${index}`,
}))
// 构建奖品池88个奖品
this.prizePool = [
...Array(25).fill('prize-1'),
...Array(18).fill('prize-2'),
...Array(2).fill('prize-3'),
...Array(3).fill('prize-4'),
...Array(8).fill('prize-5'),
...Array(15).fill('prize-6'),
...Array(17).fill('prize-7'),
]
// 重置奖品剩余数量
this.prizeConfigs.forEach((config) => {
config.remainingCount = config.totalCount
})
this.isInitialized = true
console.log(`初始化完成: ${this.personPool.length}人, ${this.prizePool.length}个奖品`)
},
// 执行一次抽奖
async executeDraw(): Promise<DrawResult | null> {
if (this.prizePool.length === 0) {
console.log('抽奖已完成')
return null
}
try {
// 从奖品池中随机抽取1个奖品
const [prizeId] = getRandomElements(this.prizePool, 1)
// 找到奖品配置
const prizeConfig = this.prizeConfigs.find(p => p.id === prizeId)
if (!prizeConfig) {
throw new Error(`未找到奖品配置: ${prizeId}`)
}
// 从奖品池中移除
const prizeIndex = this.prizePool.indexOf(prizeId)
if (prizeIndex > -1) {
this.prizePool.splice(prizeIndex, 1)
}
// 更新奖品剩余数量
prizeConfig.remainingCount--
// 记录结果
const result: DrawResult = {
id: `draw-${Date.now()}-${Math.random()}`,
drawIndex: ++this.currentDrawIndex,
prizeId,
prizeName: prizeConfig.name,
prizeDescription: prizeConfig.description,
drawTime: new Date().toISOString(),
}
this.drawResults.unshift(result)
console.log(`抽奖结果: ${result.prizeName}`)
return result
}
catch (error) {
console.error('抽奖失败:', error)
return null
}
},
// 撤销最后一次抽奖
undoLastDraw() {
if (this.drawResults.length === 0) {
console.log('没有可撤销的抽奖记录')
return false
}
const lastResult = this.drawResults[0]
// 找回奖品
this.prizePool.push(lastResult.prizeId)
// 恢复奖品数量
const prizeConfig = this.prizeConfigs.find(p => p.id === lastResult.prizeId)
if (prizeConfig) {
prizeConfig.remainingCount++
}
// 移除记录
this.drawResults.shift()
this.currentDrawIndex--
console.log(`已撤销: ${lastResult.prizeName}`)
return true
},
// 重置所有数据
reset() {
this.prizePool = []
this.personPool = []
this.drawResults = []
this.currentDrawIndex = 0
this.isInitialized = false
// 重新初始化
this.init()
console.log('已重置抽奖系统')
},
// 导出结果为Excel
exportToExcel() {
if (this.drawResults.length === 0) {
console.log('没有抽奖记录可导出')
return
}
// 动态导入xlsx
import('xlsx').then((XLSX) => {
// 准备数据(按抽奖顺序)
const results = this.drawResults.slice().reverse()
const data = results.map(result => ({
序号: result.drawIndex,
奖品名称: result.prizeName,
奖品描述: result.prizeDescription,
抽奖时间: new Date(result.drawTime).toLocaleString('zh-CN'),
}))
// 创建工作表
const ws = XLSX.utils.json_to_sheet(data)
// 设置列宽
ws['!cols'] = [
{ wch: 8 }, // 序号
{ wch: 12 }, // 人员编号
{ wch: 12 }, // 人员姓名
{ wch: 20 }, // 奖品名称
{ wch: 30 }, // 奖品描述
{ wch: 20 }, // 抽奖时间
]
// 创建工作簿
const wb = XLSX.utils.book_new()
XLSX.utils.book_append_sheet(wb, ws, '抽奖结果')
// 导出文件
const fileName = `抽奖结果-${new Date().toLocaleDateString('zh-CN').replace(/\//g, '-')}.xlsx`
XLSX.writeFile(wb, fileName)
console.log('已导出抽奖结果到Excel')
}).catch((error) => {
console.error('导出Excel失败:', error)
})
},
},
persist: {
enabled: true,
strategies: [
{
key: 'prizeDraw',
storage: localStorage,
},
],
},
})