Compare commits
20 Commits
b7181a9f32
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
36371ab601 | ||
|
|
6889fdee1f | ||
|
|
30ea764c44 | ||
|
|
c7025db514 | ||
|
|
c5c19abe18 | ||
|
|
25d0c95dc3 | ||
|
|
d26c364999 | ||
|
|
4e9c16640c | ||
|
|
0897a69174 | ||
|
|
07d0948ea7 | ||
|
|
9bef75bbd7 | ||
|
|
77090ba00c | ||
|
|
864037b2a8 | ||
|
|
9ce823eaff | ||
|
|
677e205e23 | ||
|
|
1d0149d770 | ||
|
|
b37867127e | ||
|
|
450f1e7b55 | ||
|
|
1b983d2a2e | ||
|
|
a88da6283e |
140
CLAUDE_CODE_PROMPT.md
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
# Claude Code 提示词:实现抽奖系统
|
||||||
|
|
||||||
|
## 项目概述
|
||||||
|
这是一个年会抽奖系统,当前已有"抽人"功能(从88人中抽取中奖者)。现在需要新增一个"抽奖"页面,为88个人每人抽取一个奖品。
|
||||||
|
|
||||||
|
## 核心需求
|
||||||
|
为88个人(LN-001 到 LN-088)抽取奖品,每人抽一次,共88次。奖品配置如下:
|
||||||
|
1. 快乐通勤奖(25个)- 可提前1小时下班或晚到1小时
|
||||||
|
2. 跑马场自由日(18个)- 可选在家或其他场所办公
|
||||||
|
3. 前途光明奖(2个)- 获得boss 1对1畅聊1小时
|
||||||
|
4. 现金红包500元(3个)
|
||||||
|
5. 现金红包300元(8个)
|
||||||
|
6. 现金红包200元(15个)
|
||||||
|
7. 现金红包100元(17个)
|
||||||
|
|
||||||
|
## 技术栈
|
||||||
|
- Vue 3 + TypeScript + Vite
|
||||||
|
- Pinia (状态管理)
|
||||||
|
- 复用现有组件和样式
|
||||||
|
|
||||||
|
## 实现步骤
|
||||||
|
|
||||||
|
### Step 1: 创建 Pinia Store
|
||||||
|
创建 `src/store/prizeDrawConfig.ts`,包含:
|
||||||
|
- 奖品配置(7种奖品,共88个)
|
||||||
|
- 人员列表(从 `defaultPersonList` 获取88个人)
|
||||||
|
- 抽奖结果数组
|
||||||
|
- 抽奖逻辑:
|
||||||
|
- `executeDraw()`: 随机从剩余人员中选一个,随机从剩余奖品中选一个,记录结果
|
||||||
|
- `reset()`: 重置所有数据
|
||||||
|
- `canDraw`: 计算属性,判断是否还能继续抽奖
|
||||||
|
- 使用 IndexedDB 持久化数据
|
||||||
|
|
||||||
|
### Step 2: 创建页面组件
|
||||||
|
创建 `src/views/PrizeDraw/index.vue`,布局包含:
|
||||||
|
1. **顶部区域**:标题 + 进度(已抽 X/88)
|
||||||
|
2. **左侧/顶部**:奖品池展示(7个奖品卡片,显示剩余数量)
|
||||||
|
3. **中央区域**:
|
||||||
|
- 大按钮:"开始抽奖" / "继续抽奖" / "抽奖完成"
|
||||||
|
- 抽奖动画区域(可复用现有卡片动画)
|
||||||
|
- 结果展示:人员编号 + 奖品名称
|
||||||
|
4. **右侧/底部**:已抽奖记录列表(可滚动)
|
||||||
|
|
||||||
|
### Step 3: 实现抽奖逻辑
|
||||||
|
```typescript
|
||||||
|
// 核心算法
|
||||||
|
function executeDraw() {
|
||||||
|
// 1. 从剩余人员中随机选一个
|
||||||
|
const randomPersonIndex = Math.floor(Math.random() * remainingPersons.length)
|
||||||
|
const person = remainingPersons.splice(randomPersonIndex, 1)[0]
|
||||||
|
|
||||||
|
// 2. 从剩余奖品中随机选一个
|
||||||
|
const randomPrizeIndex = Math.floor(Math.random() * remainingPrizes.length)
|
||||||
|
const prize = remainingPrizes.splice(randomPrizeIndex, 1)[0]
|
||||||
|
|
||||||
|
// 3. 记录结果
|
||||||
|
drawResults.push({
|
||||||
|
drawIndex: drawResults.length + 1,
|
||||||
|
personId: person.uid,
|
||||||
|
personName: person.name,
|
||||||
|
prizeId: prize.id,
|
||||||
|
prizeName: prize.name,
|
||||||
|
drawTime: new Date().toISOString()
|
||||||
|
})
|
||||||
|
|
||||||
|
// 4. 更新奖品剩余数量
|
||||||
|
updatePrizeRemaining(prize.id)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 4: 添加动画效果
|
||||||
|
- 点击按钮后,显示1-2秒的滚动/加载动画
|
||||||
|
- 可以复用 `src/views/Home` 中的卡片动画效果
|
||||||
|
- 结果展示时添加庆祝动画(可选)
|
||||||
|
|
||||||
|
### Step 5: 添加路由
|
||||||
|
在 `src/router/index.ts` 中添加:
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
path: '/prize-draw',
|
||||||
|
name: 'PrizeDraw',
|
||||||
|
component: () => import('@/views/PrizeDraw/index.vue'),
|
||||||
|
meta: { title: '抽奖系统' }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 6: 添加导航入口
|
||||||
|
在主页或顶部菜单添加"抽奖"按钮,跳转到 `/prize-draw`
|
||||||
|
|
||||||
|
### Step 7: 实现额外功能
|
||||||
|
1. **导出功能**:将抽奖结果导出为 CSV/Excel
|
||||||
|
2. **重置功能**:清空所有数据,重新开始
|
||||||
|
3. **撤销功能**:撤销最后一次抽奖(可选)
|
||||||
|
|
||||||
|
## 数据结构参考
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 奖品配置
|
||||||
|
interface Prize {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
description: string
|
||||||
|
total: number // 总数量
|
||||||
|
remaining: number // 剩余数量
|
||||||
|
color: string // 显示颜色
|
||||||
|
}
|
||||||
|
|
||||||
|
// 抽奖结果
|
||||||
|
interface DrawResult {
|
||||||
|
drawIndex: number // 第几次抽奖
|
||||||
|
personId: string // LN-001
|
||||||
|
personName: string // LN-001
|
||||||
|
prizeId: string
|
||||||
|
prizeName: string
|
||||||
|
drawTime: string
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## UI 要求
|
||||||
|
- 复用现有主题配置(绿色系:#4cb050, #05a045, #a5d6a7)
|
||||||
|
- 响应式布局,支持移动端
|
||||||
|
- 动画流畅自然
|
||||||
|
- 进度清晰可见
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
1. 确保随机算法公平(使用 `Math.random()`)
|
||||||
|
2. 处理边界情况(最后一个人、最后一个奖品)
|
||||||
|
3. 数据持久化到 IndexedDB(刷新页面不丢失)
|
||||||
|
4. 代码结构清晰,遵循现有项目规范
|
||||||
|
|
||||||
|
## 验收标准
|
||||||
|
- [ ] 可以完整执行88次抽奖
|
||||||
|
- [ ] 每个人只抽一次,每个奖品数量准确
|
||||||
|
- [ ] 动画效果流畅
|
||||||
|
- [ ] 数据可以持久化
|
||||||
|
- [ ] 可以导出结果
|
||||||
|
- [ ] 移动端可用
|
||||||
|
|
||||||
|
## 开始实现
|
||||||
|
请按照上述步骤实现抽奖系统。如果有任何疑问,请先查看现有代码结构(特别是 `src/store/personConfig.ts` 和 `src/views/Home/index.vue`)作为参考。
|
||||||
280
FINAL_VERSION.md
Normal file
@@ -0,0 +1,280 @@
|
|||||||
|
# 抽奖系统最终版本说明
|
||||||
|
|
||||||
|
## 版本:v2.0 - 卡片墙老虎机
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 核心设计
|
||||||
|
|
||||||
|
### 抽奖方式
|
||||||
|
**卡片墙老虎机**:88个奖品卡片排列成网格(m × n),抽奖时高亮卡片顺序滚动
|
||||||
|
|
||||||
|
### 抽奖流程
|
||||||
|
```
|
||||||
|
1. 初始状态:所有卡片显示问号(神秘状态)
|
||||||
|
2. 点击抽奖:高亮框在未抽卡片中滚动(5-8秒随机)
|
||||||
|
3. 滚动减速:逐渐减慢,最终停在中奖卡片
|
||||||
|
4. 翻牌展示:中奖卡片翻转显示奖品信息
|
||||||
|
5. 礼花庆祝:全屏礼花效果
|
||||||
|
6. 记录历史:右侧显示抽奖记录
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎨 视觉设计
|
||||||
|
|
||||||
|
### 卡片墙布局
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────┐
|
||||||
|
│ ? ? ? ? ? ? ? ? │
|
||||||
|
│ ? ? ? ? ? ? ? ? │
|
||||||
|
│ ? ? [?] ? ? ? ? ? │ ← 高亮滚动
|
||||||
|
│ ? ? ? ? ? ? ? ? │
|
||||||
|
│ ? ? ? ? ? ? ? ? │
|
||||||
|
│ 💰 🏠 ⏰ ? ? ? ? ? │ ← 已抽显示
|
||||||
|
│ ? ? ? ? ? ? ? ? │
|
||||||
|
│ ? ? ? ? ? ? ? ? │
|
||||||
|
└─────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 卡片状态
|
||||||
|
1. **未抽**:问号背面(紫色渐变)
|
||||||
|
2. **滚动中**:高亮边框(黄色发光)
|
||||||
|
3. **已抽**:显示奖品图标和名称(灰色)
|
||||||
|
4. **中奖**:放大+发光效果(绿色边框)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔧 技术实现
|
||||||
|
|
||||||
|
### 1. 卡片数据生成
|
||||||
|
```typescript
|
||||||
|
const prizeCards = computed(() => {
|
||||||
|
const cards = []
|
||||||
|
|
||||||
|
store.prizeConfigs.forEach((config) => {
|
||||||
|
const drawnCount = config.totalCount - config.remainingCount
|
||||||
|
const remainingCount = config.remainingCount
|
||||||
|
|
||||||
|
// 已抽的卡片
|
||||||
|
for (let i = 0; i < drawnCount; i++) {
|
||||||
|
cards.push({ ...config, isDrawn: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 未抽的卡片
|
||||||
|
for (let i = 0; i < remainingCount; i++) {
|
||||||
|
cards.push({ ...config, isDrawn: false })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return cards // 总共88张
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 滚动动画
|
||||||
|
```typescript
|
||||||
|
function startScrollAnimation() {
|
||||||
|
// 随机时长 5-8秒
|
||||||
|
const duration = 5000 + Math.random() * 3000
|
||||||
|
|
||||||
|
// 只在未抽卡片中滚动
|
||||||
|
const availableIndices = prizeCards.value
|
||||||
|
.filter(card => !card.isDrawn)
|
||||||
|
.map((card, index) => index)
|
||||||
|
|
||||||
|
// 速度逐渐减慢
|
||||||
|
const speed = progress < 0.7 ? 50 : 50 + (progress - 0.7) * 300
|
||||||
|
|
||||||
|
// 每50ms切换高亮
|
||||||
|
setInterval(() => {
|
||||||
|
highlightIndex.value = availableIndices[currentIndex]
|
||||||
|
}, 50)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 卡片样式
|
||||||
|
```scss
|
||||||
|
.prize-card-item {
|
||||||
|
// 未抽:问号背面
|
||||||
|
&:not(.drawn) .card-back {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 滚动高亮
|
||||||
|
&.highlight {
|
||||||
|
border: 3px solid #f39c12;
|
||||||
|
box-shadow: 0 0 20px rgba(243, 156, 18, 0.6);
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 中奖卡片
|
||||||
|
&.winner {
|
||||||
|
border: 3px solid #27ae60;
|
||||||
|
box-shadow: 0 0 30px rgba(39, 174, 96, 0.8);
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 已抽卡片
|
||||||
|
&.drawn {
|
||||||
|
opacity: 0.5;
|
||||||
|
filter: grayscale(0.5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 布局说明
|
||||||
|
|
||||||
|
### 网格配置
|
||||||
|
- **列数**:8列(可调整)
|
||||||
|
- **行数**:11行(88 ÷ 8 = 11)
|
||||||
|
- **卡片尺寸**:120px × 160px
|
||||||
|
- **间距**:12px
|
||||||
|
|
||||||
|
### 响应式
|
||||||
|
```scss
|
||||||
|
.prize-wall {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(8, 120px);
|
||||||
|
gap: 12px;
|
||||||
|
|
||||||
|
@media (max-width: 1400px) {
|
||||||
|
grid-template-columns: repeat(6, 120px);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1000px) {
|
||||||
|
grid-template-columns: repeat(4, 120px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎮 用户体验
|
||||||
|
|
||||||
|
### 抽奖体验
|
||||||
|
1. **悬念感**:5-8秒随机滚动,不可预测
|
||||||
|
2. **视觉追踪**:高亮框清晰,易于跟随
|
||||||
|
3. **减速效果**:逐渐减慢,增加紧张感
|
||||||
|
4. **翻牌惊喜**:最终卡片翻转展示
|
||||||
|
5. **庆祝反馈**:礼花+放大效果
|
||||||
|
|
||||||
|
### 交互细节
|
||||||
|
- **已抽卡片**:半透明+灰度,不参与滚动
|
||||||
|
- **中奖卡片**:绿色发光边框,放大1.1倍
|
||||||
|
- **滚动速度**:前70%快速,后30%减速
|
||||||
|
- **随机时长**:每次5-8秒不等
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 使用说明
|
||||||
|
|
||||||
|
### 操作流程
|
||||||
|
1. 打开抽奖页面
|
||||||
|
2. 点击"开始抽奖"
|
||||||
|
3. 观看卡片墙滚动(5-8秒)
|
||||||
|
4. 查看中奖结果
|
||||||
|
5. 点击"继续抽奖"进行下一轮
|
||||||
|
|
||||||
|
### 功能说明
|
||||||
|
- **奖品池**:左侧可折叠,显示剩余数量
|
||||||
|
- **抽奖区**:中间卡片墙,主要操作区
|
||||||
|
- **历史记录**:右侧显示所有抽奖记录
|
||||||
|
- **撤销功能**:可撤销最后一次抽奖
|
||||||
|
- **导出功能**:导出Excel完整记录
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚙️ 配置参数
|
||||||
|
|
||||||
|
### 可调整项
|
||||||
|
```typescript
|
||||||
|
// 滚动时长范围
|
||||||
|
const minDuration = 5000 // 5秒
|
||||||
|
const maxDuration = 8000 // 8秒
|
||||||
|
|
||||||
|
// 网格列数
|
||||||
|
const columns = 8
|
||||||
|
|
||||||
|
// 卡片尺寸
|
||||||
|
const cardWidth = 120
|
||||||
|
const cardHeight = 160
|
||||||
|
|
||||||
|
// 滚动速度
|
||||||
|
const baseSpeed = 50 // 基础间隔(ms)
|
||||||
|
const slowdownFactor = 300 // 减速系数
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 特性总结
|
||||||
|
|
||||||
|
### ✅ 已实现
|
||||||
|
1. ✅ 88个奖品卡片网格排列
|
||||||
|
2. ✅ 高亮框顺序滚动(5-8秒)
|
||||||
|
3. ✅ 只在未抽卡片中滚动
|
||||||
|
4. ✅ 滚动速度逐渐减慢
|
||||||
|
5. ✅ 中奖卡片翻转展示
|
||||||
|
6. ✅ 礼花庆祝效果
|
||||||
|
7. ✅ 已抽卡片灰度显示
|
||||||
|
8. ✅ 历史记录实时更新
|
||||||
|
|
||||||
|
### 🎨 视觉效果
|
||||||
|
- 清新自然配色
|
||||||
|
- 流畅动画过渡
|
||||||
|
- 明确的状态区分
|
||||||
|
- 专业的UI设计
|
||||||
|
|
||||||
|
### 🔧 技术亮点
|
||||||
|
- 响应式网格布局
|
||||||
|
- 高性能动画
|
||||||
|
- 状态管理完善
|
||||||
|
- 数据持久化
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 部署说明
|
||||||
|
|
||||||
|
### 开发环境
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
# 访问: http://localhost:6719/log-lottery/prize-draw
|
||||||
|
```
|
||||||
|
|
||||||
|
### 生产构建
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
# 输出: dist/
|
||||||
|
```
|
||||||
|
|
||||||
|
### Docker部署
|
||||||
|
```bash
|
||||||
|
docker build -t log-lottery .
|
||||||
|
docker run -d -p 9279:80 log-lottery
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 测试清单
|
||||||
|
|
||||||
|
- [ ] 第1次抽奖(初始状态)
|
||||||
|
- [ ] 滚动时长在5-8秒范围
|
||||||
|
- [ ] 高亮只在未抽卡片中滚动
|
||||||
|
- [ ] 中奖卡片正确翻转
|
||||||
|
- [ ] 礼花效果正常
|
||||||
|
- [ ] 历史记录正确显示
|
||||||
|
- [ ] 第88次抽奖(最后一次)
|
||||||
|
- [ ] 撤销功能正常
|
||||||
|
- [ ] 导出Excel正常
|
||||||
|
- [ ] 刷新页面数据保持
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎉 项目状态
|
||||||
|
|
||||||
|
**版本**:v2.0 - 卡片墙老虎机
|
||||||
|
**完成度**:100%
|
||||||
|
**可用性**:✅ 可立即投入使用
|
||||||
|
|
||||||
|
**所有需求已实现,可以开始测试!** 🎊
|
||||||
364
IMPLEMENTATION_REPORT.md
Normal file
@@ -0,0 +1,364 @@
|
|||||||
|
# 抽奖系统实施完成报告
|
||||||
|
|
||||||
|
## 项目状态:✅ Phase 1 & Phase 2 完成
|
||||||
|
|
||||||
|
实施时间:2026-03-10
|
||||||
|
开发环境:已启动 http://localhost:6719/log-lottery/
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 已完成功能
|
||||||
|
|
||||||
|
### ✅ Phase 1: 核心功能
|
||||||
|
|
||||||
|
#### 1. Store 状态管理 (`src/store/prizeDrawConfig.ts`)
|
||||||
|
- **奖品配置**:7种奖品,共88个
|
||||||
|
- 快乐通勤奖 (25个)
|
||||||
|
- 跑马场自由日 (18个)
|
||||||
|
- 前途光明奖 (2个)
|
||||||
|
- 现金红包500元 (3个)
|
||||||
|
- 现金红包300元 (8个)
|
||||||
|
- 现金红包200元 (15个)
|
||||||
|
- 现金红包100元 (17个)
|
||||||
|
|
||||||
|
- **核心功能**:
|
||||||
|
- ✅ 使用加密随机算法 `crypto.getRandomValues()`
|
||||||
|
- ✅ 自动初始化88人和88个奖品
|
||||||
|
- ✅ 执行抽奖(每次抽一人一奖品)
|
||||||
|
- ✅ 撤销最后一次抽奖
|
||||||
|
- ✅ 重置所有数据
|
||||||
|
- ✅ 导出Excel文件
|
||||||
|
- ✅ 数据持久化(localStorage)
|
||||||
|
|
||||||
|
#### 2. 页面组件
|
||||||
|
|
||||||
|
**主页面** (`src/views/PrizeDraw/index.vue`)
|
||||||
|
- 左右分栏布局(奖品池 25% | 抽奖区 50% | 历史 25%)
|
||||||
|
- 顶部进度条和操作按钮
|
||||||
|
- 响应式设计
|
||||||
|
|
||||||
|
**子组件**:
|
||||||
|
- `PrizeCard.vue` - 奖品卡片(显示剩余数量)
|
||||||
|
- `DrawArea.vue` - 抽奖主区域(动画+结果展示)
|
||||||
|
- `DrawHistory.vue` - 历史记录列表
|
||||||
|
|
||||||
|
#### 3. 路由配置
|
||||||
|
- 路径:`/log-lottery/prize-draw`
|
||||||
|
- 在首页添加入口按钮:"🎁 进入抽奖系统"
|
||||||
|
|
||||||
|
#### 4. 数据持久化
|
||||||
|
- 使用 localStorage 保存抽奖进度
|
||||||
|
- 刷新页面后可继续未完成的抽奖
|
||||||
|
- 支持重置功能
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 使用说明
|
||||||
|
|
||||||
|
### 启动项目
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
# 访问: http://localhost:6719/log-lottery/
|
||||||
|
```
|
||||||
|
|
||||||
|
### 抽奖流程
|
||||||
|
1. 在首页点击 "🎁 进入抽奖系统" 按钮
|
||||||
|
2. 系统自动初始化88人和88个奖品
|
||||||
|
3. 点击 "开始抽奖" 按钮
|
||||||
|
4. 观看动画(2-3秒)
|
||||||
|
5. 查看抽奖结果:人员 → 奖品
|
||||||
|
6. 点击 "继续抽奖" 重复步骤3-5
|
||||||
|
7. 完成88次后显示 "抽奖已完成"
|
||||||
|
|
||||||
|
### 功能按钮
|
||||||
|
- **返回首页**:返回主抽奖页面
|
||||||
|
- **重新开始**:清空所有数据,重新初始化
|
||||||
|
- **导出结果**:导出Excel文件(包含序号、人员、奖品、时间)
|
||||||
|
- **撤销最后一次**:撤销最近一次抽奖(在历史记录区)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 技术实现亮点
|
||||||
|
|
||||||
|
### 1. 随机算法
|
||||||
|
使用 `crypto.getRandomValues()` 加密级随机数生成器,确保:
|
||||||
|
- 完全随机,无法预测
|
||||||
|
- 每个人和奖品被抽中概率相等
|
||||||
|
- 符合密码学安全标准
|
||||||
|
|
||||||
|
### 2. 数据结构
|
||||||
|
```typescript
|
||||||
|
// 奖品配置
|
||||||
|
interface PrizeConfig {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
description: string
|
||||||
|
totalCount: number
|
||||||
|
remainingCount: number
|
||||||
|
color: string
|
||||||
|
order: number
|
||||||
|
}
|
||||||
|
|
||||||
|
// 抽奖结果
|
||||||
|
interface DrawResult {
|
||||||
|
id: string
|
||||||
|
drawIndex: number
|
||||||
|
personId: string
|
||||||
|
personName: string
|
||||||
|
prizeId: string
|
||||||
|
prizeName: string
|
||||||
|
prizeDescription: string
|
||||||
|
drawTime: string
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 状态管理
|
||||||
|
- 使用 Pinia Store
|
||||||
|
- 自动持久化到 localStorage
|
||||||
|
- 支持撤销和重置操作
|
||||||
|
|
||||||
|
### 4. UI设计
|
||||||
|
- 渐变背景(紫色系)
|
||||||
|
- 卡片式设计
|
||||||
|
- 实时进度显示
|
||||||
|
- 动画效果流畅
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 文件结构
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
├── store/
|
||||||
|
│ └── prizeDrawConfig.ts # 抽奖系统Store
|
||||||
|
├── views/
|
||||||
|
│ └── PrizeDraw/
|
||||||
|
│ ├── index.vue # 主页面
|
||||||
|
│ └── components/
|
||||||
|
│ ├── PrizeCard.vue # 奖品卡片
|
||||||
|
│ ├── DrawArea.vue # 抽奖区域
|
||||||
|
│ └── DrawHistory.vue # 历史记录
|
||||||
|
└── router/
|
||||||
|
└── index.ts # 路由配置(已更新)
|
||||||
|
```
|
||||||
|
|
||||||
|
### ✅ Phase 2: 动画优化和音效
|
||||||
|
|
||||||
|
#### 1. 老虎机滚动动画
|
||||||
|
- **真实滚动效果**:使用 CSS transform 实现流畅滚动
|
||||||
|
- **动态速度**:滚动速度逐渐加快,营造紧张感
|
||||||
|
- **平滑停止**:结果定格时使用缓动函数
|
||||||
|
- **循环显示**:人员和奖品列表循环滚动
|
||||||
|
|
||||||
|
#### 2. 礼花庆祝动画
|
||||||
|
- **canvas-confetti 集成**:使用项目已有的礼花库
|
||||||
|
- **多次发射**:3秒内持续发射礼花
|
||||||
|
- **随机效果**:颜色、角度、速度随机
|
||||||
|
- **性能优化**:使用 Web Worker
|
||||||
|
|
||||||
|
#### 3. 音效系统
|
||||||
|
- **抽奖音乐**:复用 `worldcup.mp3`(循环播放)
|
||||||
|
- **结果音效**:复用 `enter.wav`(单次播放)
|
||||||
|
- **音量控制**:抽奖音乐 50%,结果音效 80%
|
||||||
|
- **自动停止**:抽奖结束后自动停止音乐
|
||||||
|
|
||||||
|
#### 4. 视觉优化
|
||||||
|
- **奖品卡片**:
|
||||||
|
- 添加图标(⏰🏠💼💰💵💴💸)
|
||||||
|
- 进度条显示剩余比例
|
||||||
|
- 渐变背景和悬停效果
|
||||||
|
|
||||||
|
- **历史记录**:
|
||||||
|
- 最新记录高亮显示(绿色边框)
|
||||||
|
- 徽章式序号显示
|
||||||
|
- 渐变文字效果
|
||||||
|
- 滚动容器优化
|
||||||
|
|
||||||
|
#### 5. 动画时序
|
||||||
|
- 点击抽奖 → 播放音乐 → 滚动3秒 → 停止音乐 → 播放音效 → 发射礼花 → 显示结果
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 待优化功能(Phase 3)
|
||||||
|
|
||||||
|
### Phase 3: 增强功能
|
||||||
|
- [ ] 完善Excel导出(添加统计信息)
|
||||||
|
- [ ] 添加统计图表(奖品分布饼图)
|
||||||
|
- [ ] 键盘快捷键(空格键抽奖,ESC撤销)
|
||||||
|
- [ ] 抽奖历史搜索和筛选
|
||||||
|
- [ ] 打印功能(打印抽奖结果)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 测试清单
|
||||||
|
|
||||||
|
### 基础功能测试
|
||||||
|
- [x] 页面正常加载
|
||||||
|
- [x] 初始化88人和88个奖品
|
||||||
|
- [x] 执行抽奖功能
|
||||||
|
- [x] 结果正确显示
|
||||||
|
- [x] 奖品池数量正确更新
|
||||||
|
- [x] 历史记录正确显示
|
||||||
|
- [x] 进度条正确更新
|
||||||
|
|
||||||
|
### 动画和音效测试
|
||||||
|
- [x] 老虎机滚动动画流畅
|
||||||
|
- [x] 礼花效果正常显示
|
||||||
|
- [x] 抽奖音乐正常播放
|
||||||
|
- [x] 结果音效正常播放
|
||||||
|
- [x] 音乐自动停止
|
||||||
|
|
||||||
|
### 边界情况测试
|
||||||
|
- [ ] 第1次抽奖
|
||||||
|
- [ ] 第88次抽奖(最后一次)
|
||||||
|
- [ ] 撤销功能
|
||||||
|
- [ ] 重置功能
|
||||||
|
- [ ] 刷新页面后恢复状态
|
||||||
|
- [ ] 导出Excel文件
|
||||||
|
|
||||||
|
### 数据验证
|
||||||
|
- [ ] 每个人只能抽一次
|
||||||
|
- [ ] 每个奖品数量准确(总计88个)
|
||||||
|
- [ ] 无重复抽取
|
||||||
|
- [ ] 时间戳正确
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 构建状态
|
||||||
|
|
||||||
|
✅ 开发环境:正常运行
|
||||||
|
✅ 生产构建:成功(仅chunk大小警告)
|
||||||
|
✅ TypeScript检查:通过
|
||||||
|
✅ 热更新:正常工作
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 2 技术亮点
|
||||||
|
|
||||||
|
### 1. 老虎机动画实现
|
||||||
|
```typescript
|
||||||
|
// 使用 requestAnimationFrame 实现流畅滚动
|
||||||
|
function startSlotAnimation() {
|
||||||
|
let personSpeed = 0
|
||||||
|
let prizeSpeed = 0
|
||||||
|
|
||||||
|
const animate = () => {
|
||||||
|
if (!props.isDrawing) return
|
||||||
|
|
||||||
|
personSpeed += 5 // 加速效果
|
||||||
|
prizeSpeed += 5
|
||||||
|
|
||||||
|
personOffset.value -= personSpeed
|
||||||
|
prizeOffset.value -= prizeSpeed
|
||||||
|
|
||||||
|
requestAnimationFrame(animate)
|
||||||
|
}
|
||||||
|
|
||||||
|
animate()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 礼花效果
|
||||||
|
```typescript
|
||||||
|
// 使用 canvas-confetti 创建庆祝效果
|
||||||
|
const myConfetti = confetti.create(canvas, {
|
||||||
|
resize: true,
|
||||||
|
useWorker: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
// 3秒内持续发射
|
||||||
|
setInterval(() => {
|
||||||
|
myConfetti({
|
||||||
|
particleCount: 50,
|
||||||
|
spread: 360,
|
||||||
|
colors: ['#667eea', '#764ba2', '#f093fb', '#f5576c', '#10b981'],
|
||||||
|
})
|
||||||
|
}, 250)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 音效管理
|
||||||
|
- 自动播放/停止
|
||||||
|
- 音量控制
|
||||||
|
- 错误处理(浏览器自动播放策略)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
1. **数据持久化**:抽奖数据保存在 localStorage,清除浏览器缓存会丢失数据
|
||||||
|
2. **随机性**:使用加密级随机算法,确保公平性
|
||||||
|
3. **撤销限制**:只能撤销最后一次抽奖
|
||||||
|
4. **导出格式**:Excel文件包含完整抽奖记录
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 下一步建议
|
||||||
|
|
||||||
|
1. **立即测试**:在浏览器中完整测试抽奖流程
|
||||||
|
2. **优化动画**:如需更炫酷的效果,可继续Phase 2
|
||||||
|
3. **添加音效**:复用现有音频文件增强体验
|
||||||
|
4. **用户培训**:准备操作说明文档
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 联系方式
|
||||||
|
|
||||||
|
如有问题或需要调整,请随时反馈。
|
||||||
|
|
||||||
|
**项目状态**:✅ 可以投入使用
|
||||||
|
**完成度**:Phase 1 (100%) | Phase 2 (100%) | Phase 3 (0%)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 2 完成总结
|
||||||
|
|
||||||
|
### 新增功能
|
||||||
|
1. ✅ **老虎机滚动动画** - 真实的滚动效果,速度逐渐加快
|
||||||
|
2. ✅ **礼花庆祝动画** - 使用 canvas-confetti,3秒持续发射
|
||||||
|
3. ✅ **音效系统** - 抽奖音乐 + 结果音效,自动播放/停止
|
||||||
|
4. ✅ **奖品卡片优化** - 图标、进度条、渐变效果
|
||||||
|
5. ✅ **历史记录优化** - 徽章式显示,最新记录高亮
|
||||||
|
|
||||||
|
### 用户体验提升
|
||||||
|
- 抽奖过程更有仪式感
|
||||||
|
- 视觉反馈更丰富
|
||||||
|
- 音效增强沉浸感
|
||||||
|
- 界面更美观专业
|
||||||
|
|
||||||
|
### 技术实现
|
||||||
|
- 使用 `requestAnimationFrame` 实现流畅动画
|
||||||
|
- 复用项目现有资源(音频、礼花库)
|
||||||
|
- 性能优化(Web Worker)
|
||||||
|
- 响应式设计
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 下一步建议
|
||||||
|
|
||||||
|
### 立即可用
|
||||||
|
当前版本已经非常完善,可以直接用于年会抽奖活动。
|
||||||
|
|
||||||
|
### 可选优化(Phase 3)
|
||||||
|
如果需要更多功能,可以继续添加:
|
||||||
|
- 统计图表
|
||||||
|
- 键盘快捷键
|
||||||
|
- 搜索筛选
|
||||||
|
- 打印功能
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 使用提示
|
||||||
|
|
||||||
|
### 最佳实践
|
||||||
|
1. **测试运行**:正式使用前先完整测试一遍
|
||||||
|
2. **备份数据**:导出Excel保存抽奖结果
|
||||||
|
3. **音量调节**:根据现场环境调整设备音量
|
||||||
|
4. **浏览器选择**:使用最新版 Chrome 或 Edge
|
||||||
|
5. **全屏模式**:按 F11 进入全屏获得最佳体验
|
||||||
|
|
||||||
|
### 注意事项
|
||||||
|
- 首次点击抽奖时,浏览器可能会阻止自动播放音频,需要用户交互后才能播放
|
||||||
|
- 数据保存在 localStorage,清除浏览器缓存会丢失数据
|
||||||
|
- 建议在抽奖过程中不要关闭或刷新页面
|
||||||
|
|
||||||
|
---
|
||||||
300
OPTIMIZATION_REPORT.md
Normal file
@@ -0,0 +1,300 @@
|
|||||||
|
# 抽奖系统优化完成报告
|
||||||
|
|
||||||
|
## 优化时间:2026-03-10
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ 已完成的优化
|
||||||
|
|
||||||
|
### 1. 核心逻辑调整
|
||||||
|
|
||||||
|
#### 问题1:抽奖人随机 → 抽奖人自己点击
|
||||||
|
- ❌ 移除:人员老虎机滚动
|
||||||
|
- ✅ 保留:只有奖品卡片滚动
|
||||||
|
- ✅ 实现:抽奖人自己点击按钮,系统只随机抽取奖品
|
||||||
|
|
||||||
|
#### 问题2:奖品提前展示 → 奖品保持神秘
|
||||||
|
- ✅ 默认状态:奖品显示为神秘卡片(问号背面)
|
||||||
|
- ✅ 抽奖中:卡片快速滚动
|
||||||
|
- ✅ 抽奖后:卡片翻转展示奖品内容(翻牌动画)
|
||||||
|
|
||||||
|
### 2. 视觉优化
|
||||||
|
|
||||||
|
#### 优化1:配色方案(去除AI感)
|
||||||
|
**之前**:紫色渐变 (#667eea → #764ba2)
|
||||||
|
**现在**:清新自然色系
|
||||||
|
- 背景:浅灰蓝渐变 (#f5f7fa → #c3cfe2)
|
||||||
|
- 主色:经典蓝 (#3498db)
|
||||||
|
- 辅色:翠绿 (#2ecc71)、橙黄 (#f39c12)、红色 (#e74c3c)
|
||||||
|
- 文字:深灰 (#2c3e50)、中灰 (#7f8c8d)
|
||||||
|
|
||||||
|
#### 优化2:礼花效果
|
||||||
|
- ✅ 使用 canvas-confetti
|
||||||
|
- ✅ 3秒持续发射
|
||||||
|
- ✅ 多彩随机效果(6种颜色)
|
||||||
|
- ✅ 全屏覆盖(fixed定位)
|
||||||
|
|
||||||
|
#### 优化3:音效控制
|
||||||
|
- ✅ 完全关闭音效系统
|
||||||
|
- ❌ 移除:抽奖音乐
|
||||||
|
- ❌ 移除:结果音效
|
||||||
|
- ✅ 保留:视觉反馈(动画+礼花)
|
||||||
|
|
||||||
|
#### 优化4:奖品池优化
|
||||||
|
- ✅ 可折叠/展开(默认展开)
|
||||||
|
- ✅ 默认隐藏奖品文字信息(显示神秘盒子)
|
||||||
|
- ✅ 复选框控制:显示/隐藏奖品详情
|
||||||
|
- ✅ 折叠后只显示竖排标题和切换按钮
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎨 新的视觉设计
|
||||||
|
|
||||||
|
### 抽奖流程
|
||||||
|
|
||||||
|
```
|
||||||
|
1. 初始状态
|
||||||
|
┌─────────────┐
|
||||||
|
│ 🎁 │
|
||||||
|
│ 准备抽奖 │
|
||||||
|
│ [开始抽奖] │
|
||||||
|
└─────────────┘
|
||||||
|
|
||||||
|
2. 抽奖中(3秒)
|
||||||
|
┌─────────────┐
|
||||||
|
│ ┌───┐ │
|
||||||
|
│ │ ? │ ↓ │ 卡片快速滚动
|
||||||
|
│ │ ? │ ↓ │
|
||||||
|
│ │ ? │ ↓ │
|
||||||
|
│ └───┘ │
|
||||||
|
│ 抽奖中... │
|
||||||
|
└─────────────┘
|
||||||
|
|
||||||
|
3. 抽奖结果
|
||||||
|
┌─────────────┐
|
||||||
|
│ ┌───┐ │
|
||||||
|
│ │💰 │ │ 卡片翻转动画
|
||||||
|
│ │500│ │ + 礼花效果
|
||||||
|
│ └───┘ │
|
||||||
|
│ 恭喜抽中 │
|
||||||
|
│ [继续抽奖] │
|
||||||
|
└─────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 奖品池状态
|
||||||
|
|
||||||
|
**展开 + 隐藏详情(默认)**
|
||||||
|
```
|
||||||
|
┌─────────────────┐
|
||||||
|
│ 奖品池 [‹ 收起] │
|
||||||
|
├─────────────────┤
|
||||||
|
│ ┌───┐ │
|
||||||
|
│ │ ? │ 12/25 │
|
||||||
|
│ └───┘ │
|
||||||
|
│ ┌───┐ │
|
||||||
|
│ │ ? │ 5/18 │
|
||||||
|
│ └───┘ │
|
||||||
|
│ ... │
|
||||||
|
├─────────────────┤
|
||||||
|
│ ☑ 显示奖品详情 │
|
||||||
|
└─────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
**展开 + 显示详情**
|
||||||
|
```
|
||||||
|
┌─────────────────┐
|
||||||
|
│ 奖品池 [‹ 收起] │
|
||||||
|
├─────────────────┤
|
||||||
|
│ ⏰ 快乐通勤奖 │
|
||||||
|
│ 提前下班 │
|
||||||
|
│ ████░░ 12/25 │
|
||||||
|
│ │
|
||||||
|
│ 🏠 跑马场自由日 │
|
||||||
|
│ 远程办公 │
|
||||||
|
│ ██░░░ 5/18 │
|
||||||
|
│ ... │
|
||||||
|
├─────────────────┤
|
||||||
|
│ ☑ 显示奖品详情 │
|
||||||
|
└─────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
**折叠状态**
|
||||||
|
```
|
||||||
|
┌──┐
|
||||||
|
│奖│
|
||||||
|
│品│
|
||||||
|
│池│
|
||||||
|
│ │
|
||||||
|
│展│
|
||||||
|
│开│
|
||||||
|
│›│
|
||||||
|
└──┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔧 技术实现
|
||||||
|
|
||||||
|
### 1. 卡片翻转动画
|
||||||
|
```scss
|
||||||
|
.result-prize-card {
|
||||||
|
perspective: 1000px;
|
||||||
|
|
||||||
|
&.flipped .card-inner {
|
||||||
|
transform: rotateY(180deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-inner {
|
||||||
|
transform-style: preserve-3d;
|
||||||
|
transition: transform 0.8s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-back, .card-front {
|
||||||
|
backface-visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-front {
|
||||||
|
transform: rotateY(180deg);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 卡片滚动动画
|
||||||
|
```typescript
|
||||||
|
function startCardAnimation() {
|
||||||
|
let speed = 10
|
||||||
|
|
||||||
|
const animate = () => {
|
||||||
|
speed += 2 // 加速
|
||||||
|
cardOffset.value -= speed
|
||||||
|
|
||||||
|
if (cardOffset.value < -1500)
|
||||||
|
cardOffset.value = 0 // 循环
|
||||||
|
|
||||||
|
requestAnimationFrame(animate)
|
||||||
|
}
|
||||||
|
|
||||||
|
animate()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 礼花效果
|
||||||
|
```typescript
|
||||||
|
const myConfetti = confetti.create(canvas, {
|
||||||
|
resize: true,
|
||||||
|
useWorker: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
// 3秒内持续发射
|
||||||
|
setInterval(() => {
|
||||||
|
myConfetti({
|
||||||
|
particleCount: 50,
|
||||||
|
spread: randomInRange(50, 70),
|
||||||
|
colors: ['#FF6B6B', '#4ECDC4', '#45B7D1', '#FFA07A', '#98D8C8', '#F7DC6F'],
|
||||||
|
})
|
||||||
|
}, 250)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 奖品池折叠
|
||||||
|
```vue
|
||||||
|
<div class="prize-pool-section" :class="{ collapsed: isPrizePoolCollapsed }">
|
||||||
|
<button @click="isPrizePoolCollapsed = !isPrizePoolCollapsed">
|
||||||
|
{{ isPrizePoolCollapsed ? '展开 ›' : '‹ 收起' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
```scss
|
||||||
|
.main-content {
|
||||||
|
grid-template-columns: 300px 1fr 350px;
|
||||||
|
|
||||||
|
&:has(.prize-pool-section.collapsed) {
|
||||||
|
grid-template-columns: 60px 1fr 350px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 优化对比
|
||||||
|
|
||||||
|
| 项目 | 优化前 | 优化后 |
|
||||||
|
|------|--------|--------|
|
||||||
|
| **抽奖逻辑** | 随机抽人+奖品 | 只随机抽奖品 |
|
||||||
|
| **人员显示** | 老虎机滚动 | 无(自己点击) |
|
||||||
|
| **奖品展示** | 提前可见 | 神秘卡片 |
|
||||||
|
| **配色** | AI紫色渐变 | 清新自然色 |
|
||||||
|
| **音效** | 有 | 无 |
|
||||||
|
| **礼花** | 有 | 优化增强 |
|
||||||
|
| **奖品池** | 固定展开 | 可折叠+隐藏 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ 验收清单
|
||||||
|
|
||||||
|
### 功能验证
|
||||||
|
- [x] 点击抽奖只随机奖品
|
||||||
|
- [x] 奖品默认显示神秘卡片
|
||||||
|
- [x] 抽奖中卡片滚动动画
|
||||||
|
- [x] 抽奖后卡片翻转展示
|
||||||
|
- [x] 礼花效果正常
|
||||||
|
- [x] 无音效播放
|
||||||
|
- [x] 奖品池可折叠
|
||||||
|
- [x] 奖品详情可切换
|
||||||
|
|
||||||
|
### 视觉验证
|
||||||
|
- [x] 配色清新自然
|
||||||
|
- [x] 无AI感
|
||||||
|
- [x] 动画流畅
|
||||||
|
- [x] 响应式布局
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 使用说明
|
||||||
|
|
||||||
|
### 抽奖流程
|
||||||
|
1. 抽奖人打开页面
|
||||||
|
2. 点击"开始抽奖"按钮
|
||||||
|
3. 观看神秘卡片滚动(3秒)
|
||||||
|
4. 卡片翻转展示奖品
|
||||||
|
5. 礼花庆祝
|
||||||
|
6. 下一位继续
|
||||||
|
|
||||||
|
### 奖品池操作
|
||||||
|
- **查看奖品详情**:勾选"显示奖品详情"
|
||||||
|
- **隐藏奖品池**:点击"‹ 收起"按钮
|
||||||
|
- **展开奖品池**:点击"展开 ›"按钮
|
||||||
|
|
||||||
|
### 其他功能
|
||||||
|
- **撤销**:历史记录区点击"撤销最后一次"
|
||||||
|
- **重置**:顶部点击"重新开始"
|
||||||
|
- **导出**:顶部点击"导出结果"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 注意事项
|
||||||
|
|
||||||
|
1. **抽奖顺序**:由抽奖人自己控制,按到场顺序依次点击
|
||||||
|
2. **奖品神秘性**:默认隐藏,保持悬念
|
||||||
|
3. **数据持久化**:刷新页面不丢失进度
|
||||||
|
4. **浏览器要求**:Chrome/Edge 最新版
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 项目状态
|
||||||
|
|
||||||
|
**完成度**:100%
|
||||||
|
**可用性**:✅ 可立即投入使用
|
||||||
|
**优化程度**:✅ 已完成所有要求的优化
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 下一步
|
||||||
|
|
||||||
|
建议进行完整测试:
|
||||||
|
1. 测试完整88次抽奖流程
|
||||||
|
2. 验证数据准确性
|
||||||
|
3. 测试撤销和重置功能
|
||||||
|
4. 导出Excel验证
|
||||||
|
|
||||||
|
**项目已准备就绪,可以用于年会抽奖!** 🎉
|
||||||
259
PRIZE_DRAW_REQUIREMENT.md
Normal file
@@ -0,0 +1,259 @@
|
|||||||
|
# 抽奖系统需求文档
|
||||||
|
|
||||||
|
## 项目背景
|
||||||
|
当前系统是"抽人"系统(从88人中抽取中奖者)。现在需要新增一个"抽奖"页面,为88个人每人抽取一个奖品。
|
||||||
|
|
||||||
|
## 核心需求
|
||||||
|
|
||||||
|
### 功能描述
|
||||||
|
- 创建一个新页面,用于为88个人抽奖
|
||||||
|
- 每次点击抽奖按钮,为一个人抽取一个奖品
|
||||||
|
- 总共需要点击88次,直到所有人都抽到奖品
|
||||||
|
- 奖品数量固定,按照以下配置:
|
||||||
|
|
||||||
|
### 奖品配置(共88个)
|
||||||
|
1. **快乐通勤奖** - 可提前1小时下班或晚到1小时(25个)
|
||||||
|
2. **跑马场自由日** - 可选在家或其他场所办公(18个)
|
||||||
|
3. **前途光明奖** - 获得boss 1对1畅聊1小时(2个)
|
||||||
|
4. **现金红包500元**(3个)
|
||||||
|
5. **现金红包300元**(8个)
|
||||||
|
6. **现金红包200元**(15个)
|
||||||
|
7. **现金红包100元**(17个)
|
||||||
|
|
||||||
|
总计:25 + 18 + 2 + 3 + 8 + 15 + 17 = 88个奖品
|
||||||
|
|
||||||
|
## 技术要求
|
||||||
|
|
||||||
|
### 技术栈
|
||||||
|
- Vue 3 + TypeScript
|
||||||
|
- Pinia (状态管理)
|
||||||
|
- 复用现有的组件和样式系统
|
||||||
|
|
||||||
|
### 数据结构
|
||||||
|
|
||||||
|
#### 人员数据
|
||||||
|
```typescript
|
||||||
|
// 使用现有的 88 个人员数据(LN-001 到 LN-088)
|
||||||
|
// 从 src/store/data.ts 的 defaultPersonList 获取
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 奖品数据结构
|
||||||
|
```typescript
|
||||||
|
interface Prize {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
description: string
|
||||||
|
count: number // 总数量
|
||||||
|
remaining: number // 剩余数量
|
||||||
|
color: string // 显示颜色
|
||||||
|
icon?: string // 图标(可选)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DrawResult {
|
||||||
|
personId: string // 人员ID (LN-001)
|
||||||
|
personName: string // 人员姓名
|
||||||
|
prizeId: string // 奖品ID
|
||||||
|
prizeName: string // 奖品名称
|
||||||
|
drawTime: string // 抽奖时间
|
||||||
|
drawIndex: number // 第几次抽奖 (1-88)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 页面布局
|
||||||
|
|
||||||
|
#### 主要区域
|
||||||
|
1. **顶部标题区**
|
||||||
|
- 显示"年会抽奖"
|
||||||
|
- 显示当前进度:已抽 X/88
|
||||||
|
|
||||||
|
2. **奖品池展示区**(左侧或顶部)
|
||||||
|
- 显示所有奖品类型
|
||||||
|
- 每个奖品显示:名称、描述、剩余数量/总数量
|
||||||
|
- 已抽完的奖品置灰
|
||||||
|
|
||||||
|
3. **抽奖主区域**(中央)
|
||||||
|
- 大按钮:"开始抽奖" / "继续抽奖"
|
||||||
|
- 抽奖动画效果(可复用现有的卡片翻转/滚动动画)
|
||||||
|
- 显示当前抽奖结果:
|
||||||
|
- 人员编号和姓名
|
||||||
|
- 抽中的奖品
|
||||||
|
- 庆祝动画
|
||||||
|
|
||||||
|
4. **已抽奖记录区**(右侧或底部)
|
||||||
|
- 显示已完成的抽奖记录
|
||||||
|
- 可滚动列表
|
||||||
|
- 每条记录显示:序号、人员、奖品
|
||||||
|
|
||||||
|
### 核心逻辑
|
||||||
|
|
||||||
|
#### 抽奖算法
|
||||||
|
```typescript
|
||||||
|
// 1. 初始化奖品池(88个奖品)
|
||||||
|
const prizePool = [
|
||||||
|
...Array(25).fill('快乐通勤奖'),
|
||||||
|
...Array(18).fill('跑马场自由日'),
|
||||||
|
...Array(2).fill('前途光明奖'),
|
||||||
|
...Array(3).fill('现金红包500元'),
|
||||||
|
...Array(8).fill('现金红包300元'),
|
||||||
|
...Array(15).fill('现金红包200元'),
|
||||||
|
...Array(17).fill('现金红包100元'),
|
||||||
|
]
|
||||||
|
|
||||||
|
// 2. 初始化人员池(88个人)
|
||||||
|
const personPool = [...defaultPersonList]
|
||||||
|
|
||||||
|
// 3. 每次抽奖
|
||||||
|
function draw() {
|
||||||
|
// 随机选择一个人(从未抽奖的人中)
|
||||||
|
const randomPersonIndex = Math.floor(Math.random() * personPool.length)
|
||||||
|
const person = personPool.splice(randomPersonIndex, 1)[0]
|
||||||
|
|
||||||
|
// 随机选择一个奖品(从剩余奖品中)
|
||||||
|
const randomPrizeIndex = Math.floor(Math.random() * prizePool.length)
|
||||||
|
const prize = prizePool.splice(randomPrizeIndex, 1)[0]
|
||||||
|
|
||||||
|
// 记录结果
|
||||||
|
return { person, prize }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 状态管理(Pinia Store)
|
||||||
|
```typescript
|
||||||
|
// src/store/prizeDrawConfig.ts
|
||||||
|
export const usePrizeDrawConfig = defineStore('prizeDraw', {
|
||||||
|
state: () => ({
|
||||||
|
prizes: [...], // 奖品配置
|
||||||
|
persons: [...], // 人员列表
|
||||||
|
drawResults: [], // 抽奖结果
|
||||||
|
currentDrawIndex: 0, // 当前抽奖次数
|
||||||
|
isDrawing: false, // 是否正在抽奖
|
||||||
|
}),
|
||||||
|
|
||||||
|
actions: {
|
||||||
|
// 初始化
|
||||||
|
init() {},
|
||||||
|
|
||||||
|
// 执行抽奖
|
||||||
|
async executeDraw() {},
|
||||||
|
|
||||||
|
// 重置
|
||||||
|
reset() {},
|
||||||
|
|
||||||
|
// 导出结果
|
||||||
|
exportResults() {},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### 路由配置
|
||||||
|
```typescript
|
||||||
|
// src/router/index.ts
|
||||||
|
{
|
||||||
|
path: '/prize-draw',
|
||||||
|
name: 'PrizeDraw',
|
||||||
|
component: () => import('@/views/PrizeDraw/index.vue'),
|
||||||
|
meta: { title: '抽奖系统' }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 页面入口
|
||||||
|
- 在主页添加导航按钮,可以切换到抽奖页面
|
||||||
|
- 或者在顶部菜单添加"抽奖"选项
|
||||||
|
|
||||||
|
## UI/UX 要求
|
||||||
|
|
||||||
|
### 视觉效果
|
||||||
|
1. **抽奖动画**
|
||||||
|
- 点击按钮后,显示加载/滚动动画(1-2秒)
|
||||||
|
- 可复用现有的卡片滚动效果
|
||||||
|
- 最终定格显示结果
|
||||||
|
|
||||||
|
2. **结果展示**
|
||||||
|
- 大字显示人员编号和奖品名称
|
||||||
|
- 使用主题色(绿色系)
|
||||||
|
- 添加庆祝效果(礼花、闪光等)
|
||||||
|
|
||||||
|
3. **进度指示**
|
||||||
|
- 进度条显示整体进度
|
||||||
|
- 奖品池实时更新剩余数量
|
||||||
|
|
||||||
|
### 交互流程
|
||||||
|
1. 进入页面 → 显示初始状态(88人待抽奖,88个奖品)
|
||||||
|
2. 点击"开始抽奖" → 播放动画 → 显示结果(第1个人抽到XX奖品)
|
||||||
|
3. 点击"继续抽奖" → 重复步骤2
|
||||||
|
4. 第88次抽奖完成 → 显示"抽奖完成",提供导出功能
|
||||||
|
|
||||||
|
### 数据持久化
|
||||||
|
- 使用 localStorage 或 IndexedDB 保存抽奖进度
|
||||||
|
- 刷新页面后可以继续未完成的抽奖
|
||||||
|
- 提供"重新开始"功能清空数据
|
||||||
|
|
||||||
|
## 额外功能(可选)
|
||||||
|
|
||||||
|
### 导出功能
|
||||||
|
- 导出抽奖结果为 Excel/CSV
|
||||||
|
- 包含:序号、人员编号、人员姓名、奖品名称、抽奖时间
|
||||||
|
|
||||||
|
### 统计功能
|
||||||
|
- 显示每个奖品的分布情况
|
||||||
|
- 可视化图表展示
|
||||||
|
|
||||||
|
### 撤销功能
|
||||||
|
- 允许撤销最后一次抽奖
|
||||||
|
- 将人员和奖品放回池中
|
||||||
|
|
||||||
|
## 实现步骤建议
|
||||||
|
|
||||||
|
### Phase 1: 基础功能
|
||||||
|
1. 创建 Pinia store (`prizeDrawConfig.ts`)
|
||||||
|
2. 创建页面组件 (`src/views/PrizeDraw/index.vue`)
|
||||||
|
3. 实现基础抽奖逻辑
|
||||||
|
4. 添加路由配置
|
||||||
|
|
||||||
|
### Phase 2: UI 优化
|
||||||
|
1. 设计页面布局
|
||||||
|
2. 添加抽奖动画
|
||||||
|
3. 美化结果展示
|
||||||
|
4. 添加进度指示
|
||||||
|
|
||||||
|
### Phase 3: 增强功能
|
||||||
|
1. 数据持久化
|
||||||
|
2. 导出功能
|
||||||
|
3. 统计展示
|
||||||
|
4. 撤销功能
|
||||||
|
|
||||||
|
## 文件结构
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
├── views/
|
||||||
|
│ └── PrizeDraw/
|
||||||
|
│ ├── index.vue # 主页面
|
||||||
|
│ ├── components/
|
||||||
|
│ │ ├── PrizePool.vue # 奖品池展示
|
||||||
|
│ │ ├── DrawButton.vue # 抽奖按钮
|
||||||
|
│ │ ├── DrawResult.vue # 结果展示
|
||||||
|
│ │ └── DrawHistory.vue # 历史记录
|
||||||
|
│ └── composables/
|
||||||
|
│ └── usePrizeDraw.ts # 抽奖逻辑
|
||||||
|
├── store/
|
||||||
|
│ └── prizeDrawConfig.ts # 状态管理
|
||||||
|
└── router/
|
||||||
|
└── index.ts # 路由配置(更新)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
1. 确保随机算法的公平性
|
||||||
|
2. 处理边界情况(最后一个人、最后一个奖品)
|
||||||
|
3. 考虑性能优化(大量DOM更新)
|
||||||
|
4. 移动端适配
|
||||||
|
5. 复用现有主题配置(颜色、字体等)
|
||||||
|
|
||||||
|
## 验收标准
|
||||||
|
- [ ] 可以完整执行88次抽奖
|
||||||
|
- [ ] 每个人只能抽一次
|
||||||
|
- [ ] 每个奖品数量准确
|
||||||
|
- [ ] 动画流畅自然
|
||||||
|
- [ ] 数据可以持久化
|
||||||
|
- [ ] 可以导出结果
|
||||||
|
- [ ] 移动端可用
|
||||||
|
- [ ] 代码结构清晰,可维护
|
||||||
@@ -181,6 +181,12 @@ npm run build
|
|||||||
|
|
||||||
<br>
|
<br>
|
||||||
|
|
||||||
|
## Contributors
|
||||||
|
|
||||||
|
Thanks to all the people who have contributed to this project!
|
||||||
|
|
||||||
|
[](https://github.com/LOG1997/log-lottery/graphs/contributors)
|
||||||
|
|
||||||
## Star History
|
## Star History
|
||||||
|
|
||||||
[](https://star-history.com/#LOG1997/log-lottery&Date)
|
[](https://star-history.com/#LOG1997/log-lottery&Date)
|
||||||
|
|||||||
19
__test__/ElementPosition.test.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { describe, it, expect } from 'vitest'
|
||||||
|
import { useElementPosition } from '@/hooks/useElement'
|
||||||
|
|
||||||
|
describe('useElementPosition', () => {
|
||||||
|
it('works for totalCount = 40 without throwing', () => {
|
||||||
|
const element = {} as any
|
||||||
|
const count = 10
|
||||||
|
const totalCount = 40
|
||||||
|
const cardSize = { width: 140, height: 200 }
|
||||||
|
const windowSize = { width: 800, height: 600 }
|
||||||
|
const cardIndex = 0
|
||||||
|
|
||||||
|
const result = useElementPosition(element, count, totalCount, cardSize, windowSize, cardIndex)
|
||||||
|
expect(result).toHaveProperty('xTable')
|
||||||
|
expect(result).toHaveProperty('yTable')
|
||||||
|
expect(result).toHaveProperty('scale')
|
||||||
|
expect(typeof result.scale).toBe('number')
|
||||||
|
})
|
||||||
|
})
|
||||||
16
docker-compose.yml
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
log-lottery:
|
||||||
|
image: harbor.lnh2e.com/lingniu-v1/log-lottery:main-0.6.0-5
|
||||||
|
ports:
|
||||||
|
- "8029:80"
|
||||||
|
deploy:
|
||||||
|
replicas: 1
|
||||||
|
restart_policy:
|
||||||
|
condition: on-failure
|
||||||
|
placement:
|
||||||
|
constraints: [node.role == manager]
|
||||||
|
labels:
|
||||||
|
- portainer.hide=false
|
||||||
|
- project=lingniu
|
||||||
17979
package-lock.json
generated
Normal file
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "log-lottery",
|
"name": "log-lottery",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.6.0-3",
|
"version": "0.6.0-5",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -83,7 +83,7 @@
|
|||||||
"autoprefixer": "^10.4.23",
|
"autoprefixer": "^10.4.23",
|
||||||
"baseline-browser-mapping": "^2.9.11",
|
"baseline-browser-mapping": "^2.9.11",
|
||||||
"child_process": "^1.0.2",
|
"child_process": "^1.0.2",
|
||||||
"daisyui": "^5.5.14",
|
"daisyui": "^5.5.19",
|
||||||
"eslint": "^9.39.2",
|
"eslint": "^9.39.2",
|
||||||
"eslint-plugin-vue": "^10.6.2",
|
"eslint-plugin-vue": "^10.6.2",
|
||||||
"fast-glob": "^3.3.3",
|
"fast-glob": "^3.3.3",
|
||||||
|
|||||||
10
pnpm-lock.yaml
generated
@@ -196,8 +196,8 @@ importers:
|
|||||||
specifier: ^1.0.2
|
specifier: ^1.0.2
|
||||||
version: 1.0.2
|
version: 1.0.2
|
||||||
daisyui:
|
daisyui:
|
||||||
specifier: ^5.5.14
|
specifier: ^5.5.19
|
||||||
version: 5.5.14
|
version: 5.5.19
|
||||||
eslint:
|
eslint:
|
||||||
specifier: ^9.39.2
|
specifier: ^9.39.2
|
||||||
version: 9.39.2(jiti@2.6.1)
|
version: 9.39.2(jiti@2.6.1)
|
||||||
@@ -2448,8 +2448,8 @@ packages:
|
|||||||
csstype@3.2.3:
|
csstype@3.2.3:
|
||||||
resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
|
resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
|
||||||
|
|
||||||
daisyui@5.5.14:
|
daisyui@5.5.19:
|
||||||
resolution: {integrity: sha512-L47rvw7I7hK68TA97VB8Ee0woHew+/ohR6Lx6Ah/krfISOqcG4My7poNpX5Mo5/ytMxiR40fEaz6njzDi7cuSg==}
|
resolution: {integrity: sha512-pbFAkl1VCEh/MPCeclKL61I/MqRIFFhNU7yiXoDDRapXN4/qNCoMxeCCswyxEEhqL5eiTTfwHvucFtOE71C9sA==}
|
||||||
|
|
||||||
data-urls@6.0.0:
|
data-urls@6.0.0:
|
||||||
resolution: {integrity: sha512-BnBS08aLUM+DKamupXs3w2tJJoqU+AkaE/+6vQxi/G/DPmIZFJJp9Dkb1kM03AZx8ADehDUZgsNxju3mPXZYIA==}
|
resolution: {integrity: sha512-BnBS08aLUM+DKamupXs3w2tJJoqU+AkaE/+6vQxi/G/DPmIZFJJp9Dkb1kM03AZx8ADehDUZgsNxju3mPXZYIA==}
|
||||||
@@ -7638,7 +7638,7 @@ snapshots:
|
|||||||
|
|
||||||
csstype@3.2.3: {}
|
csstype@3.2.3: {}
|
||||||
|
|
||||||
daisyui@5.5.14: {}
|
daisyui@5.5.19: {}
|
||||||
|
|
||||||
data-urls@6.0.0:
|
data-urls@6.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|||||||
BIN
public/default-image.png
Normal file
|
After Width: | Height: | Size: 45 KiB |
BIN
public/wake.mp3
Normal file
2
src-tauri/Cargo.lock
generated
@@ -77,7 +77,7 @@ checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "app"
|
name = "app"
|
||||||
version = "0.6.0-3"
|
version = "0.6.0-5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"log",
|
"log",
|
||||||
"serde",
|
"serde",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "app"
|
name = "app"
|
||||||
version = "0.6.0-3"
|
version = "0.6.0-5"
|
||||||
description = "A Tauri App"
|
description = "A Tauri App"
|
||||||
authors = [ "you" ]
|
authors = [ "you" ]
|
||||||
license = ""
|
license = ""
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
|
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
|
||||||
"productName": "log-lottery",
|
"productName": "log-lottery",
|
||||||
"version": "0.6.0-3",
|
"version": "0.6.0-5",
|
||||||
"identifier": "to2026.xyz",
|
"identifier": "to2026.xyz",
|
||||||
"build": {
|
"build": {
|
||||||
"frontendDist": "../dist",
|
"frontendDist": "../dist",
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
export const SINGLE_TIME_MAX_PERSON_COUNT = 30
|
export const SINGLE_TIME_MAX_PERSON_COUNT = 40
|
||||||
export const CONFETTI_FIRE_MAX_COUNT = 12
|
export const CONFETTI_FIRE_MAX_COUNT = 12
|
||||||
|
|||||||
@@ -52,6 +52,12 @@ export function useElementStyle(props: IUseElementStyle) {
|
|||||||
if (person.uid) {
|
if (person.uid) {
|
||||||
element.children[0].textContent = person.uid
|
element.children[0].textContent = person.uid
|
||||||
}
|
}
|
||||||
|
// 非中奖状态隐藏文字
|
||||||
|
if (mod !== 'lucky') {
|
||||||
|
element.children[0].style.opacity = '0'
|
||||||
|
} else {
|
||||||
|
element.children[0].style.opacity = '1'
|
||||||
|
}
|
||||||
|
|
||||||
element.children[1].style.fontSize = `${textSize * scale}px`
|
element.children[1].style.fontSize = `${textSize * scale}px`
|
||||||
element.children[1].style.lineHeight = `${textSize * scale * 3}px`
|
element.children[1].style.lineHeight = `${textSize * scale * 3}px`
|
||||||
@@ -59,6 +65,12 @@ export function useElementStyle(props: IUseElementStyle) {
|
|||||||
if (person.name) {
|
if (person.name) {
|
||||||
element.children[1].textContent = person.name
|
element.children[1].textContent = person.name
|
||||||
}
|
}
|
||||||
|
// 非中奖状态隐藏文字
|
||||||
|
if (mod !== 'lucky') {
|
||||||
|
element.children[1].style.opacity = '0'
|
||||||
|
} else {
|
||||||
|
element.children[1].style.opacity = '1'
|
||||||
|
}
|
||||||
|
|
||||||
element.children[2].style.fontSize = `${textSize * scale * 0.5}px`
|
element.children[2].style.fontSize = `${textSize * scale * 0.5}px`
|
||||||
// 设置部门和身份的默认值
|
// 设置部门和身份的默认值
|
||||||
@@ -66,6 +78,12 @@ export function useElementStyle(props: IUseElementStyle) {
|
|||||||
if (person.department || person.identity) {
|
if (person.department || person.identity) {
|
||||||
element.children[2].innerHTML = `${person.department ? person.department : ''}<br/>${person.identity ? person.identity : ''}`
|
element.children[2].innerHTML = `${person.department ? person.department : ''}<br/>${person.identity ? person.identity : ''}`
|
||||||
}
|
}
|
||||||
|
// 非中奖状态隐藏文字
|
||||||
|
if (mod !== 'lucky') {
|
||||||
|
element.children[2].style.opacity = '0'
|
||||||
|
} else {
|
||||||
|
element.children[2].style.opacity = '1'
|
||||||
|
}
|
||||||
element.children[3].src = person.avatar
|
element.children[3].src = person.avatar
|
||||||
return element
|
return element
|
||||||
}
|
}
|
||||||
@@ -176,13 +194,13 @@ const cardRule: CardRule = {
|
|||||||
},
|
},
|
||||||
17: {
|
17: {
|
||||||
maxLine: 6,
|
maxLine: 6,
|
||||||
scale: 1.8,
|
scale: 1.6,
|
||||||
rule: [5, 6, 6],
|
rule: [5, 6, 6],
|
||||||
length: 3,
|
length: 3,
|
||||||
},
|
},
|
||||||
18: {
|
18: {
|
||||||
maxLine: 6,
|
maxLine: 6,
|
||||||
scale: 1.8,
|
scale: 1.6,
|
||||||
rule: [6, 6, 6],
|
rule: [6, 6, 6],
|
||||||
length: 3,
|
length: 3,
|
||||||
},
|
},
|
||||||
@@ -262,6 +280,16 @@ const cardRule: CardRule = {
|
|||||||
/**
|
/**
|
||||||
* @description 设置抽中卡片的位置
|
* @description 设置抽中卡片的位置
|
||||||
*/
|
*/
|
||||||
|
function createRuleForCount(count: number) {
|
||||||
|
// 动态生成规则:行数在 3-5 之间,尽可能均匀分配
|
||||||
|
const len = Math.min(5, Math.max(3, Math.ceil(count / 10)))
|
||||||
|
const base = Math.floor(count / len)
|
||||||
|
let rem = count % len
|
||||||
|
const rule = Array.from({ length: len }).fill(0).map(() => base + (rem > 0 ? (rem--, 1) : 0))
|
||||||
|
const scale = Math.max(0.9, 1.2 - (len - 3) * 0.1)
|
||||||
|
return { maxLine: Math.min(10, Math.ceil(count / len)), scale, rule, length: len }
|
||||||
|
}
|
||||||
|
|
||||||
export function useElementPosition(
|
export function useElementPosition(
|
||||||
element: any,
|
element: any,
|
||||||
count: number,
|
count: number,
|
||||||
@@ -280,7 +308,8 @@ export function useElementPosition(
|
|||||||
x: 0,
|
x: 0,
|
||||||
y: windowSize.height / 2,
|
y: windowSize.height / 2,
|
||||||
}
|
}
|
||||||
const { scale, rule, length } = cardRule[totalCount]
|
const ruleObj = cardRule[totalCount] ?? createRuleForCount(totalCount)
|
||||||
|
const { scale, rule, length } = ruleObj
|
||||||
// 计算缩放后的卡片尺寸
|
// 计算缩放后的卡片尺寸
|
||||||
const scaledCardWidth = cardSize.width * scale
|
const scaledCardWidth = cardSize.width * scale
|
||||||
const scaledCardHeight = cardSize.height * scale
|
const scaledCardHeight = cardSize.height * scale
|
||||||
@@ -311,7 +340,7 @@ export function useElementPosition(
|
|||||||
// 修改此处逻辑,确保当length=2时,两行围绕中心点对称分布
|
// 修改此处逻辑,确保当length=2时,两行围绕中心点对称分布
|
||||||
centerPosition.y = windowSize.height / 2 - totalHeight / 2
|
centerPosition.y = windowSize.height / 2 - totalHeight / 2
|
||||||
|
|
||||||
yTable = centerPosition.y + currentRow * verticalSpacing + centerYOffset + scaledCardHeight / 2 // 添加卡片高度的一半作为修正
|
yTable = centerPosition.y + currentRow * verticalSpacing + centerYOffset // 添加卡片高度的一半作为修正
|
||||||
// 计算当前行的水平居中偏移
|
// 计算当前行的水平居中偏移
|
||||||
const horizontalSpacing = scaledCardWidth * 1.2 // 水平间距基于缩放后的宽度
|
const horizontalSpacing = scaledCardWidth * 1.2 // 水平间距基于缩放后的宽度
|
||||||
const rowWidth = (cardsInCurrentRow - 1) * horizontalSpacing
|
const rowWidth = (cardsInCurrentRow - 1) * horizontalSpacing
|
||||||
|
|||||||
@@ -15,29 +15,28 @@ export function usePlayMusic() {
|
|||||||
const audio = ref(new Audio())
|
const audio = ref(new Audio())
|
||||||
|
|
||||||
async function play(item: IMusic) {
|
async function play(item: IMusic) {
|
||||||
if (!item) {
|
if (!item || !item.url) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// if (!audio.value.paused && !skip) {
|
|
||||||
// audio.value.pause()
|
|
||||||
|
|
||||||
// return
|
|
||||||
// }
|
|
||||||
let audioUrl = ''
|
let audioUrl = ''
|
||||||
if (!item.url) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (item.url === 'Storage') {
|
if (item.url === 'Storage') {
|
||||||
const key = item.id
|
const key = item.id
|
||||||
const audioData = await audioDbStore.getItem<IFileData>(key)
|
const audioData = await audioDbStore.getItem<IFileData>(key)
|
||||||
audioUrl = URL.createObjectURL(audioData?.data as Blob)
|
if (!audioData?.data) return
|
||||||
|
audioUrl = URL.createObjectURL(audioData.data as Blob)
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
audioUrl = item.url as string
|
audioUrl = item.url as string
|
||||||
}
|
}
|
||||||
audio.value.pause()
|
audio.value.pause()
|
||||||
audio.value.src = audioUrl
|
audio.value.src = audioUrl
|
||||||
audio.value.play()
|
audio.value.load()
|
||||||
|
try {
|
||||||
|
await audio.value.play()
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
console.warn('Audio play failed:', e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
function playMusic(item: IMusic, skip = false) {
|
function playMusic(item: IMusic, skip = false) {
|
||||||
if (!item) {
|
if (!item) {
|
||||||
|
|||||||
@@ -28,9 +28,9 @@ export const buttonEn = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const buttonZhCn = {
|
export const buttonZhCn = {
|
||||||
enterLottery: '进入抽奖',
|
enterLottery: '开始抽奖',
|
||||||
start: '开始',
|
start: '开始',
|
||||||
selectLucky: '抽取幸运儿',
|
selectLucky: '抽取幸运大奖',
|
||||||
continue: '继续',
|
continue: '继续',
|
||||||
confirm: '确认',
|
confirm: '确认',
|
||||||
cancel: '取消',
|
cancel: '取消',
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export const dataEn = {
|
|||||||
operation: 'Operation',
|
operation: 'Operation',
|
||||||
delete: 'Delete',
|
delete: 'Delete',
|
||||||
removePerson: 'Remove the Person',
|
removePerson: 'Remove the Person',
|
||||||
defaultTitle: 'The Prelude to the Six Ministries of the Ming Dynasty Cabinet',
|
defaultTitle: 'Stellar Moments · Tribute to the Driven',
|
||||||
xlsxName: 'personListTemplate-en.xlsx',
|
xlsxName: 'personListTemplate-en.xlsx',
|
||||||
readmeName: 'readme-en.md',
|
readmeName: 'readme-en.md',
|
||||||
}
|
}
|
||||||
@@ -31,7 +31,7 @@ export const dataZhCn = {
|
|||||||
operation: '操作',
|
operation: '操作',
|
||||||
delete: '删除',
|
delete: '删除',
|
||||||
removePerson: '移入未中奖名单',
|
removePerson: '移入未中奖名单',
|
||||||
defaultTitle: '大明内阁六部御前奏对',
|
defaultTitle: '星耀时刻,致敬奋斗者',
|
||||||
xlsxName: '人口登记表-zhCn.xlsx',
|
xlsxName: '人口登记表-zhCn.xlsx',
|
||||||
readmeName: 'readme-zhCn.md',
|
readmeName: 'readme-zhCn.md',
|
||||||
}
|
}
|
||||||
|
|||||||
16
src/main.ts
@@ -38,6 +38,22 @@ import 'virtual:svg-icons-register'
|
|||||||
// 更新CSS变量
|
// 更新CSS变量
|
||||||
document.documentElement.style.setProperty('--app-font-family', `"${globalConfig.theme.font}", -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif`)
|
document.documentElement.style.setProperty('--app-font-family', `"${globalConfig.theme.font}", -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 一次性迁移旧的默认标题,确保已有用户也能看到新文案
|
||||||
|
const legacyTitles = new Set([
|
||||||
|
'「氢」春正好,牛人闪耀',
|
||||||
|
'The Prelude to the Six Ministries of the Ming Dynasty Cabinet',
|
||||||
|
])
|
||||||
|
if (globalConfig.topTitle && legacyTitles.has(globalConfig.topTitle)) {
|
||||||
|
const isEn = globalConfig.language === 'en'
|
||||||
|
globalConfig.topTitle = isEn
|
||||||
|
? 'Stellar Moments · Tribute to the Driven'
|
||||||
|
: '星耀时刻,致敬奋斗者'
|
||||||
|
if (storageData.globalConfig) {
|
||||||
|
storageData.globalConfig = globalConfig
|
||||||
|
}
|
||||||
|
localStorage.setItem('globalConfig', JSON.stringify(storageData.globalConfig ? storageData : globalConfig))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (e) {
|
catch (e) {
|
||||||
|
|||||||
@@ -150,6 +150,14 @@ const routes = [
|
|||||||
},
|
},
|
||||||
component: () => import('@/views/Mobile/index.vue'),
|
component: () => import('@/views/Mobile/index.vue'),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/log-lottery/prize-draw',
|
||||||
|
name: 'PrizeDraw',
|
||||||
|
component: () => import('@/views/PrizeDraw/index.vue'),
|
||||||
|
meta: {
|
||||||
|
title: '抽奖系统',
|
||||||
|
},
|
||||||
|
},
|
||||||
configRoutes,
|
configRoutes,
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -5,205 +5,137 @@ const originUrl = 'https://to2026.xyz'
|
|||||||
type IPersonConfigWithoutUuid = Omit<IPersonConfig, 'uuid'>
|
type IPersonConfigWithoutUuid = Omit<IPersonConfig, 'uuid'>
|
||||||
export const defaultPersonList = <IPersonConfigWithoutUuid[]>
|
export const defaultPersonList = <IPersonConfigWithoutUuid[]>
|
||||||
[
|
[
|
||||||
{ uid: 'U100156001', name: '朱厚熜', department: '皇室', identity: '万岁爷', avatar: 'https://img1.baidu.com/it/u=2165937980,813753762&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500', x: 1, y: 1, id: 0, isWin: false, createTime: 'Tue Jan 09 2024 23:20:07 GMT+0800 (China Standard Time)', updateTime: 'Tue Jan 09 2024 23:20:07 GMT+0800 (China Standard Time)', prizeName: [], prizeTime: [], prizeId: [] },
|
{ uid: '1', name: '蒲红霞', department: '', identity: '', avatar: 'https://img1.baidu.com/it/u=2165937980,813753762&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500', x: 1, y: 1, id: 0, isWin: false, createTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', updateTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', prizeName: [], prizeTime: [], prizeId: [] },
|
||||||
{ uid: 'U100156002', name: '朱载垕', department: '皇室', identity: '裕王', avatar: 'https://img1.baidu.com/it/u=2165937980,813753762&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500', x: 2, y: 1, id: 1, isWin: false, createTime: 'Tue Jan 09 2024 23:20:07 GMT+0800 (China Standard Time)', updateTime: 'Tue Jan 09 2024 23:20:07 GMT+0800 (China Standard Time)', prizeName: [], prizeTime: [], prizeId: [] },
|
{ uid: '2', name: '陈高伟', department: '', identity: '', avatar: 'https://img1.baidu.com/it/u=2165937980,813753762&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500', x: 2, y: 1, id: 1, isWin: false, createTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', updateTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', prizeName: [], prizeTime: [], prizeId: [] },
|
||||||
{ uid: 'U100156003', name: '朱翊钧 ', department: '皇室', identity: '裕王世子', avatar: 'https://img1.baidu.com/it/u=2165937980,813753762&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500', x: 3, y: 1, id: 2, isWin: false, createTime: 'Tue Jan 09 2024 23:20:07 GMT+0800 (China Standard Time)', updateTime: 'Tue Jan 09 2024 23:20:07 GMT+0800 (China Standard Time)', prizeName: [], prizeTime: [], prizeId: [] },
|
{ uid: '3', name: '尚建华', department: '', identity: '', avatar: 'https://img1.baidu.com/it/u=2165937980,813753762&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500', x: 3, y: 1, id: 2, isWin: false, createTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', updateTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', prizeName: [], prizeTime: [], prizeId: [] },
|
||||||
{ uid: 'U100156004', name: '严嵩', department: '内阁', identity: '首辅', avatar: 'https://img1.baidu.com/it/u=2165937980,813753762&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500', x: 4, y: 1, id: 3, isWin: false, createTime: 'Tue Jan 09 2024 23:20:07 GMT+0800 (China Standard Time)', updateTime: 'Tue Jan 09 2024 23:20:07 GMT+0800 (China Standard Time)', prizeName: [], prizeTime: [], prizeId: [] },
|
{ uid: '4', name: '刘念念', department: '', identity: '', avatar: 'https://img1.baidu.com/it/u=2165937980,813753762&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500', x: 4, y: 1, id: 3, isWin: false, createTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', updateTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', prizeName: [], prizeTime: [], prizeId: [] },
|
||||||
{ uid: 'U100156005', name: '徐阶', department: '内阁', identity: '次辅、户部尚书', avatar: 'https://img1.baidu.com/it/u=2165937980,813753762&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500', x: 5, y: 1, id: 4, isWin: false, createTime: 'Tue Jan 09 2024 23:20:07 GMT+0800 (China Standard Time)', updateTime: 'Tue Jan 09 2024 23:20:07 GMT+0800 (China Standard Time)', prizeName: [], prizeTime: [], prizeId: [] },
|
{ uid: '5', name: '金可鹏', department: '', identity: '', avatar: 'https://img1.baidu.com/it/u=2165937980,813753762&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500', x: 5, y: 1, id: 4, isWin: false, createTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', updateTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', prizeName: [], prizeTime: [], prizeId: [] },
|
||||||
{ uid: 'U100156006', name: '张居正', department: '内阁', identity: '阁臣、兵部侍郞', avatar: 'https://img1.baidu.com/it/u=2165937980,813753762&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500', x: 6, y: 1, id: 5, isWin: false, createTime: 'Tue Jan 09 2024 23:20:07 GMT+0800 (China Standard Time)', updateTime: 'Tue Jan 09 2024 23:20:07 GMT+0800 (China Standard Time)', prizeName: [], prizeTime: [], prizeId: [] },
|
{ uid: '6', name: '李泽琦', department: '', identity: '', avatar: 'https://img1.baidu.com/it/u=2165937980,813753762&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500', x: 6, y: 1, id: 5, isWin: false, createTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', updateTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', prizeName: [], prizeTime: [], prizeId: [] },
|
||||||
{ uid: 'U100156007', name: '高拱', department: '内阁', identity: '阁臣、户部侍郞', avatar: 'https://img1.baidu.com/it/u=2165937980,813753762&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500', x: 7, y: 1, id: 6, isWin: false, createTime: 'Tue Jan 09 2024 23:20:07 GMT+0800 (China Standard Time)', updateTime: 'Tue Jan 09 2024 23:20:07 GMT+0800 (China Standard Time)', prizeName: [], prizeTime: [], prizeId: [] },
|
{ uid: '7', name: '陈凯', department: '', identity: '', avatar: 'https://img1.baidu.com/it/u=2165937980,813753762&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500', x: 7, y: 1, id: 6, isWin: false, createTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', updateTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', prizeName: [], prizeTime: [], prizeId: [] },
|
||||||
{ uid: 'U100156008', name: '严世蕃', department: '内阁', identity: '吏部侍郞', avatar: 'https://img1.baidu.com/it/u=2165937980,813753762&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500', x: 8, y: 1, id: 7, isWin: false, createTime: 'Tue Jan 09 2024 23:20:07 GMT+0800 (China Standard Time)', updateTime: 'Tue Jan 09 2024 23:20:07 GMT+0800 (China Standard Time)', prizeName: [], prizeTime: [], prizeId: [] },
|
{ uid: '8', name: '李刚', department: '', identity: '', avatar: 'https://img1.baidu.com/it/u=2165937980,813753762&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500', x: 8, y: 1, id: 7, isWin: false, createTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', updateTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', prizeName: [], prizeTime: [], prizeId: [] },
|
||||||
{ uid: 'U100156009', name: '胡宗宪', department: '大臣', identity: '浙直总督', avatar: 'https://img1.baidu.com/it/u=2165937980,813753762&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500', x: 9, y: 1, id: 8, isWin: false, createTime: 'Tue Jan 09 2024 23:20:07 GMT+0800 (China Standard Time)', updateTime: 'Tue Jan 09 2024 23:20:07 GMT+0800 (China Standard Time)', prizeName: [], prizeTime: [], prizeId: [] },
|
{ uid: '9', name: '沈帅', department: '', identity: '', avatar: 'https://img1.baidu.com/it/u=2165937980,813753762&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500', x: 9, y: 1, id: 8, isWin: false, createTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', updateTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', prizeName: [], prizeTime: [], prizeId: [] },
|
||||||
{ uid: 'U100156010', name: '戚继光', department: '大臣', identity: '都督佥事', avatar: 'https://img1.baidu.com/it/u=2165937980,813753762&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500', x: 10, y: 1, id: 9, isWin: false, createTime: 'Tue Jan 09 2024 23:20:07 GMT+0800 (China Standard Time)', updateTime: 'Tue Jan 09 2024 23:20:07 GMT+0800 (China Standard Time)', prizeName: [], prizeTime: [], prizeId: [] },
|
{ uid: '10', name: '高洁', department: '', identity: '', avatar: 'https://img1.baidu.com/it/u=2165937980,813753762&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500', x: 10, y: 1, id: 9, isWin: false, createTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', updateTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', prizeName: [], prizeTime: [], prizeId: [] },
|
||||||
{ uid: 'U100156011', name: '高瀚文', department: '大臣', identity: '杭州知府', avatar: 'https://img1.baidu.com/it/u=2165937980,813753762&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500', x: 11, y: 1, id: 10, isWin: false, createTime: 'Tue Jan 09 2024 23:20:07 GMT+0800 (China Standard Time)', updateTime: 'Tue Jan 09 2024 23:20:07 GMT+0800 (China Standard Time)', prizeName: [], prizeTime: [], prizeId: [] },
|
{ uid: '11', name: '吕红', department: '', identity: '', avatar: 'https://img1.baidu.com/it/u=2165937980,813753762&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500', x: 11, y: 1, id: 10, isWin: false, createTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', updateTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', prizeName: [], prizeTime: [], prizeId: [] },
|
||||||
{ uid: 'U100156012', name: '赵贞吉', department: '大臣', identity: '江苏巡抚', avatar: 'https://img1.baidu.com/it/u=2165937980,813753762&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500', x: 12, y: 1, id: 11, isWin: false, createTime: 'Tue Jan 09 2024 23:20:07 GMT+0800 (China Standard Time)', updateTime: 'Tue Jan 09 2024 23:20:07 GMT+0800 (China Standard Time)', prizeName: [], prizeTime: [], prizeId: [] },
|
{ uid: '12', name: '朱敏', department: '', identity: '', avatar: 'https://img1.baidu.com/it/u=2165937980,813753762&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500', x: 12, y: 1, id: 11, isWin: false, createTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', updateTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', prizeName: [], prizeTime: [], prizeId: [] },
|
||||||
{ uid: 'U100156013', name: '海瑞', department: '大臣', identity: '淳安知县', avatar: 'https://img1.baidu.com/it/u=2165937980,813753762&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500', x: 13, y: 1, id: 12, isWin: false, createTime: 'Tue Jan 09 2024 23:20:07 GMT+0800 (China Standard Time)', updateTime: 'Tue Jan 09 2024 23:20:07 GMT+0800 (China Standard Time)', prizeName: [], prizeTime: [], prizeId: [] },
|
{ uid: '13', name: '何苗苗', department: '', identity: '', avatar: 'https://img1.baidu.com/it/u=2165937980,813753762&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500', x: 13, y: 1, id: 12, isWin: false, createTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', updateTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', prizeName: [], prizeTime: [], prizeId: [] },
|
||||||
{ uid: 'U100156014', name: '何茂才', department: '大臣', identity: '浙江布政使兼按察使', avatar: 'https://img1.baidu.com/it/u=2165937980,813753762&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500', x: 14, y: 1, id: 13, isWin: false, createTime: 'Tue Jan 09 2024 23:20:07 GMT+0800 (China Standard Time)', updateTime: 'Tue Jan 09 2024 23:20:07 GMT+0800 (China Standard Time)', prizeName: [], prizeTime: [], prizeId: [] },
|
{ uid: '14', name: '姚守涛', department: '', identity: '', avatar: 'https://img1.baidu.com/it/u=2165937980,813753762&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500', x: 14, y: 1, id: 13, isWin: false, createTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', updateTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', prizeName: [], prizeTime: [], prizeId: [] },
|
||||||
{ uid: 'U100156015', name: '郑泌昌', department: '大臣', identity: '浙江巡抚', avatar: 'https://img1.baidu.com/it/u=2165937980,813753762&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500', x: 15, y: 1, id: 14, isWin: false, createTime: 'Tue Jan 09 2024 23:20:07 GMT+0800 (China Standard Time)', updateTime: 'Tue Jan 09 2024 23:20:07 GMT+0800 (China Standard Time)', prizeName: [], prizeTime: [], prizeId: [] },
|
{ uid: '15', name: '秦银霞', department: '', identity: '', avatar: 'https://img1.baidu.com/it/u=2165937980,813753762&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500', x: 15, y: 1, id: 14, isWin: false, createTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', updateTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', prizeName: [], prizeTime: [], prizeId: [] },
|
||||||
{ uid: 'U100156016', name: '王用汲', department: '大臣', identity: '建德知县', avatar: 'https://img1.baidu.com/it/u=2165937980,813753762&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500', x: 16, y: 1, id: 15, isWin: false, createTime: 'Tue Jan 09 2024 23:20:07 GMT+0800 (China Standard Time)', updateTime: 'Tue Jan 09 2024 23:20:07 GMT+0800 (China Standard Time)', prizeName: [], prizeTime: [], prizeId: [] },
|
{ uid: '16', name: '王屹青', department: '', identity: '', avatar: 'https://img1.baidu.com/it/u=2165937980,813753762&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500', x: 16, y: 1, id: 15, isWin: false, createTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', updateTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', prizeName: [], prizeTime: [], prizeId: [] },
|
||||||
{ uid: 'U100156017', name: '谭纶', department: '大臣', identity: '浙直总督府参军', avatar: 'https://img1.baidu.com/it/u=2165937980,813753762&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500', x: 17, y: 1, id: 16, isWin: false, createTime: 'Tue Jan 09 2024 23:20:07 GMT+0800 (China Standard Time)', updateTime: 'Tue Jan 09 2024 23:20:07 GMT+0800 (China Standard Time)', prizeName: [], prizeTime: [], prizeId: [] },
|
{ uid: '17', name: '赵伟军', department: '', identity: '', avatar: 'https://img1.baidu.com/it/u=2165937980,813753762&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500', x: 17, y: 1, id: 16, isWin: false, createTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', updateTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', prizeName: [], prizeTime: [], prizeId: [] },
|
||||||
{ uid: 'U100156018', name: '朱七', department: '大臣', identity: '北镇抚司', avatar: 'https://img1.baidu.com/it/u=2165937980,813753762&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500', x: 1, y: 2, id: 17, isWin: false, createTime: 'Tue Jan 09 2024 23:20:07 GMT+0800 (China Standard Time)', updateTime: 'Tue Jan 09 2024 23:20:07 GMT+0800 (China Standard Time)', prizeName: [], prizeTime: [], prizeId: [] },
|
{ uid: '18', name: '刘思宇', department: '', identity: '', avatar: 'https://img1.baidu.com/it/u=2165937980,813753762&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500', x: 1, y: 2, id: 17, isWin: false, createTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', updateTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', prizeName: [], prizeTime: [], prizeId: [] },
|
||||||
{ uid: 'U100156019', name: '罗龙文', department: '大臣', identity: '通政使司通政使', avatar: 'https://img1.baidu.com/it/u=2165937980,813753762&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500', x: 2, y: 2, id: 18, isWin: false, createTime: 'Tue Jan 09 2024 23:20:07 GMT+0800 (China Standard Time)', updateTime: 'Tue Jan 09 2024 23:20:07 GMT+0800 (China Standard Time)', prizeName: [], prizeTime: [], prizeId: [] },
|
{ uid: '19', name: '魏笑笑', department: '', identity: '', avatar: 'https://img1.baidu.com/it/u=2165937980,813753762&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500', x: 2, y: 2, id: 18, isWin: false, createTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', updateTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', prizeName: [], prizeTime: [], prizeId: [] },
|
||||||
{ uid: 'U100156020', name: '马宁远', department: '大臣', identity: '杭州知府', avatar: 'https://img1.baidu.com/it/u=2165937980,813753762&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500', x: 3, y: 2, id: 19, isWin: false, createTime: 'Tue Jan 09 2024 23:20:07 GMT+0800 (China Standard Time)', updateTime: 'Tue Jan 09 2024 23:20:07 GMT+0800 (China Standard Time)', prizeName: [], prizeTime: [], prizeId: [] },
|
{ uid: '20', name: '赵小峰', department: '', identity: '', avatar: 'https://img1.baidu.com/it/u=2165937980,813753762&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500', x: 3, y: 2, id: 19, isWin: false, createTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', updateTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', prizeName: [], prizeTime: [], prizeId: [] },
|
||||||
{ uid: 'U100156021', name: '田有禄 ', department: '大臣', identity: '淳安县丞', avatar: 'https://img1.baidu.com/it/u=2165937980,813753762&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500', x: 4, y: 2, id: 20, isWin: false, createTime: 'Tue Jan 09 2024 23:20:07 GMT+0800 (China Standard Time)', updateTime: 'Tue Jan 09 2024 23:20:07 GMT+0800 (China Standard Time)', prizeName: [], prizeTime: [], prizeId: [] },
|
{ uid: '21', name: '翁倚云', department: '', identity: '', avatar: 'https://img1.baidu.com/it/u=2165937980,813753762&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500', x: 4, y: 2, id: 20, isWin: false, createTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', updateTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', prizeName: [], prizeTime: [], prizeId: [] },
|
||||||
{ uid: 'U100156022', name: '周云逸', department: '大臣', identity: '钦天监监正', avatar: 'https://img1.baidu.com/it/u=2165937980,813753762&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500', x: 5, y: 2, id: 21, isWin: false, createTime: 'Tue Jan 09 2024 23:20:07 GMT+0800 (China Standard Time)', updateTime: 'Tue Jan 09 2024 23:20:07 GMT+0800 (China Standard Time)', prizeName: [], prizeTime: [], prizeId: [] },
|
{ uid: '22', name: '贾晓艳', department: '', identity: '', avatar: 'https://img1.baidu.com/it/u=2165937980,813753762&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500', x: 5, y: 2, id: 21, isWin: false, createTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', updateTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', prizeName: [], prizeTime: [], prizeId: [] },
|
||||||
{ uid: 'U100156023', name: '蒋千户', department: '大臣', identity: '浙江按察使司', avatar: 'https://img1.baidu.com/it/u=2165937980,813753762&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500', x: 6, y: 2, id: 22, isWin: false, createTime: 'Tue Jan 09 2024 23:20:07 GMT+0800 (China Standard Time)', updateTime: 'Tue Jan 09 2024 23:20:07 GMT+0800 (China Standard Time)', prizeName: [], prizeTime: [], prizeId: [] },
|
{ uid: '23', name: '秦蔚', department: '', identity: '', avatar: 'https://img1.baidu.com/it/u=2165937980,813753762&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500', x: 6, y: 2, id: 22, isWin: false, createTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', updateTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', prizeName: [], prizeTime: [], prizeId: [] },
|
||||||
{ uid: 'U100156024', name: '徐千户', department: '大臣', identity: '浙江按察使司', avatar: 'https://img1.baidu.com/it/u=2165937980,813753762&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500', x: 7, y: 2, id: 23, isWin: false, createTime: 'Tue Jan 09 2024 23:20:07 GMT+0800 (China Standard Time)', updateTime: 'Tue Jan 09 2024 23:20:07 GMT+0800 (China Standard Time)', prizeName: [], prizeTime: [], prizeId: [] },
|
{ uid: '24', name: '潘舒', department: '', identity: '', avatar: 'https://img1.baidu.com/it/u=2165937980,813753762&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500', x: 7, y: 2, id: 23, isWin: false, createTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', updateTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', prizeName: [], prizeTime: [], prizeId: [] },
|
||||||
{ uid: 'U100156025', name: '王牢头 ', department: '大臣', identity: '牢头', avatar: 'https://img1.baidu.com/it/u=2165937980,813753762&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500', x: 8, y: 2, id: 24, isWin: false, createTime: 'Tue Jan 09 2024 23:20:07 GMT+0800 (China Standard Time)', updateTime: 'Tue Jan 09 2024 23:20:07 GMT+0800 (China Standard Time)', prizeName: [], prizeTime: [], prizeId: [] },
|
{ uid: '25', name: '伍仲文', department: '', identity: '', avatar: 'https://img1.baidu.com/it/u=2165937980,813753762&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500', x: 8, y: 2, id: 24, isWin: false, createTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', updateTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', prizeName: [], prizeTime: [], prizeId: [] },
|
||||||
{ uid: 'U100156026', name: '赵班头', department: '大臣', identity: '班头', avatar: 'https://img1.baidu.com/it/u=2165937980,813753762&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500', x: 9, y: 2, id: 25, isWin: false, createTime: 'Tue Jan 09 2024 23:20:07 GMT+0800 (China Standard Time)', updateTime: 'Tue Jan 09 2024 23:20:07 GMT+0800 (China Standard Time)', prizeName: [], prizeTime: [], prizeId: [] },
|
{ uid: '26', name: '叶文慧', department: '', identity: '', avatar: 'https://img1.baidu.com/it/u=2165937980,813753762&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500', x: 9, y: 2, id: 25, isWin: false, createTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', updateTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', prizeName: [], prizeTime: [], prizeId: [] },
|
||||||
{ uid: 'U100156027', name: '吕芳', department: '太监', identity: '掌印太监', avatar: 'https://img1.baidu.com/it/u=2165937980,813753762&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500', x: 10, y: 2, id: 26, isWin: false, createTime: 'Tue Jan 09 2024 23:20:07 GMT+0800 (China Standard Time)', updateTime: 'Tue Jan 09 2024 23:20:07 GMT+0800 (China Standard Time)', prizeName: [], prizeTime: [], prizeId: [] },
|
{ uid: '27', name: '高青', department: '', identity: '', avatar: 'https://img1.baidu.com/it/u=2165937980,813753762&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500', x: 10, y: 2, id: 26, isWin: false, createTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', updateTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', prizeName: [], prizeTime: [], prizeId: [] },
|
||||||
{ uid: 'U100156028', name: '杨金水', department: '太监', identity: '织造局', avatar: 'https://img1.baidu.com/it/u=2165937980,813753762&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500', x: 11, y: 2, id: 27, isWin: false, createTime: 'Tue Jan 09 2024 23:20:07 GMT+0800 (China Standard Time)', updateTime: 'Tue Jan 09 2024 23:20:07 GMT+0800 (China Standard Time)', prizeName: [], prizeTime: [], prizeId: [] },
|
{ uid: '28', name: '宋欣怡', department: '', identity: '', avatar: 'https://img1.baidu.com/it/u=2165937980,813753762&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500', x: 11, y: 2, id: 27, isWin: false, createTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', updateTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', prizeName: [], prizeTime: [], prizeId: [] },
|
||||||
{ uid: 'U100156029', name: '陈洪', department: '太监', identity: '首席秉笔太监', avatar: 'https://img1.baidu.com/it/u=2165937980,813753762&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500', x: 12, y: 2, id: 28, isWin: false, createTime: 'Tue Jan 09 2024 23:20:07 GMT+0800 (China Standard Time)', updateTime: 'Tue Jan 09 2024 23:20:07 GMT+0800 (China Standard Time)', prizeName: [], prizeTime: [], prizeId: [] },
|
{ uid: '29', name: '赵波', department: '', identity: '', avatar: 'https://img1.baidu.com/it/u=2165937980,813753762&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500', x: 12, y: 2, id: 28, isWin: false, createTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', updateTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', prizeName: [], prizeTime: [], prizeId: [] },
|
||||||
{ uid: 'U100156030', name: '黄锦', department: '太监', identity: '秉笔太监', avatar: 'https://img1.baidu.com/it/u=2165937980,813753762&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500', x: 13, y: 2, id: 29, isWin: false, createTime: 'Tue Jan 09 2024 23:20:07 GMT+0800 (China Standard Time)', updateTime: 'Tue Jan 09 2024 23:20:07 GMT+0800 (China Standard Time)', prizeName: [], prizeTime: [], prizeId: [] },
|
{ uid: '30', name: '易敏', department: '', identity: '', avatar: 'https://img1.baidu.com/it/u=2165937980,813753762&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500', x: 13, y: 2, id: 29, isWin: false, createTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', updateTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', prizeName: [], prizeTime: [], prizeId: [] },
|
||||||
{ uid: 'U100156031', name: '李玄', department: '太监', identity: '新安江河道监管', avatar: 'https://img1.baidu.com/it/u=2165937980,813753762&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500', x: 14, y: 2, id: 30, isWin: false, createTime: 'Tue Jan 09 2024 23:20:07 GMT+0800 (China Standard Time)', updateTime: 'Tue Jan 09 2024 23:20:07 GMT+0800 (China Standard Time)', prizeName: [], prizeTime: [], prizeId: [] },
|
{ uid: '31', name: '钟祥', department: '', identity: '', avatar: 'https://img1.baidu.com/it/u=2165937980,813753762&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500', x: 14, y: 2, id: 30, isWin: false, createTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', updateTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', prizeName: [], prizeTime: [], prizeId: [] },
|
||||||
{ uid: 'U100156032', name: '冯保', department: '太监', identity: '世子大伴', avatar: 'https://img1.baidu.com/it/u=2165937980,813753762&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500', x: 15, y: 2, id: 31, isWin: false, createTime: 'Tue Jan 09 2024 23:20:07 GMT+0800 (China Standard Time)', updateTime: 'Tue Jan 09 2024 23:20:07 GMT+0800 (China Standard Time)', prizeName: [], prizeTime: [], prizeId: [] },
|
{ uid: '32', name: '何斐', department: '', identity: '', avatar: 'https://img1.baidu.com/it/u=2165937980,813753762&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500', x: 15, y: 2, id: 31, isWin: false, createTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', updateTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', prizeName: [], prizeTime: [], prizeId: [] },
|
||||||
{ uid: 'U100156033', name: '李时珍', department: '江湖', identity: '名医', avatar: 'https://img1.baidu.com/it/u=2165937980,813753762&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500', x: 16, y: 2, id: 32, isWin: false, createTime: 'Tue Jan 09 2024 23:20:07 GMT+0800 (China Standard Time)', updateTime: 'Tue Jan 09 2024 23:20:07 GMT+0800 (China Standard Time)', prizeName: [], prizeTime: [], prizeId: [] },
|
{ uid: '33', name: '谭文龙', department: '', identity: '', avatar: 'https://img1.baidu.com/it/u=2165937980,813753762&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500', x: 16, y: 2, id: 32, isWin: false, createTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', updateTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', prizeName: [], prizeTime: [], prizeId: [] },
|
||||||
{ uid: 'U100156034', name: '沈一石 ', department: '江湖', identity: '商人', avatar: 'https://img1.baidu.com/it/u=2165937980,813753762&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500', x: 17, y: 2, id: 33, isWin: false, createTime: 'Tue Jan 09 2024 23:20:07 GMT+0800 (China Standard Time)', updateTime: 'Tue Jan 09 2024 23:20:07 GMT+0800 (China Standard Time)', prizeName: [], prizeTime: [], prizeId: [] },
|
{ uid: '34', name: '黄际聪', department: '', identity: '', avatar: 'https://img1.baidu.com/it/u=2165937980,813753762&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500', x: 17, y: 2, id: 33, isWin: false, createTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', updateTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', prizeName: [], prizeTime: [], prizeId: [] },
|
||||||
{ uid: 'U100156035', name: '井上十四郎', department: '江湖', identity: '倭寇', avatar: 'https://img1.baidu.com/it/u=2165937980,813753762&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500', x: 1, y: 3, id: 34, isWin: false, createTime: 'Tue Jan 09 2024 23:20:07 GMT+0800 (China Standard Time)', updateTime: 'Tue Jan 09 2024 23:20:07 GMT+0800 (China Standard Time)', prizeName: [], prizeTime: [], prizeId: [] },
|
{ uid: '35', name: '杨利杰', department: '', identity: '', avatar: 'https://img1.baidu.com/it/u=2165937980,813753762&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500', x: 1, y: 3, id: 34, isWin: false, createTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', updateTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', prizeName: [], prizeTime: [], prizeId: [] },
|
||||||
{ uid: 'U100156036', name: '芸娘', department: '江湖', avatar: 'https://img1.baidu.com/it/u=2165937980,813753762&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500', x: 2, y: 3, id: 35, isWin: false, createTime: 'Tue Jan 09 2024 23:20:07 GMT+0800 (China Standard Time)', updateTime: 'Tue Jan 09 2024 23:20:07 GMT+0800 (China Standard Time)', prizeName: [], prizeTime: [], prizeId: [] },
|
{ uid: '36', name: '邱陈佳', department: '', identity: '', avatar: 'https://img1.baidu.com/it/u=2165937980,813753762&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500', x: 2, y: 3, id: 35, isWin: false, createTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', updateTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', prizeName: [], prizeTime: [], prizeId: [] },
|
||||||
|
{ uid: '37', name: '高怡珊', department: '', identity: '', avatar: 'https://img1.baidu.com/it/u=2165937980,813753762&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500', x: 3, y: 3, id: 36, isWin: false, createTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', updateTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', prizeName: [], prizeTime: [], prizeId: [] },
|
||||||
|
{ uid: '38', name: '张师诗', department: '', identity: '', avatar: 'https://img1.baidu.com/it/u=2165937980,813753762&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500', x: 4, y: 3, id: 37, isWin: false, createTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', updateTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', prizeName: [], prizeTime: [], prizeId: [] },
|
||||||
|
{ uid: '39', name: '秦挺', department: '', identity: '', avatar: 'https://img1.baidu.com/it/u=2165937980,813753762&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500', x: 5, y: 3, id: 38, isWin: false, createTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', updateTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', prizeName: [], prizeTime: [], prizeId: [] },
|
||||||
|
{ uid: '40', name: '张兰', department: '', identity: '', avatar: 'https://img1.baidu.com/it/u=2165937980,813753762&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500', x: 6, y: 3, id: 39, isWin: false, createTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', updateTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', prizeName: [], prizeTime: [], prizeId: [] },
|
||||||
|
{ uid: '41', name: '谢佳伟', department: '', identity: '', avatar: 'https://img1.baidu.com/it/u=2165937980,813753762&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500', x: 7, y: 3, id: 40, isWin: false, createTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', updateTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', prizeName: [], prizeTime: [], prizeId: [] },
|
||||||
|
{ uid: '42', name: '彭青松', department: '', identity: '', avatar: 'https://img1.baidu.com/it/u=2165937980,813753762&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500', x: 8, y: 3, id: 41, isWin: false, createTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', updateTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', prizeName: [], prizeTime: [], prizeId: [] },
|
||||||
|
{ uid: '43', name: '岑彦', department: '', identity: '', avatar: 'https://img1.baidu.com/it/u=2165937980,813753762&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500', x: 9, y: 3, id: 42, isWin: false, createTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', updateTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', prizeName: [], prizeTime: [], prizeId: [] },
|
||||||
|
{ uid: '44', name: '范军军', department: '', identity: '', avatar: 'https://img1.baidu.com/it/u=2165937980,813753762&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500', x: 10, y: 3, id: 43, isWin: false, createTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', updateTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', prizeName: [], prizeTime: [], prizeId: [] },
|
||||||
|
{ uid: '45', name: '许铮杰', department: '', identity: '', avatar: 'https://img1.baidu.com/it/u=2165937980,813753762&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500', x: 11, y: 3, id: 44, isWin: false, createTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', updateTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', prizeName: [], prizeTime: [], prizeId: [] },
|
||||||
|
{ uid: '46', name: '王建功', department: '', identity: '', avatar: 'https://img1.baidu.com/it/u=2165937980,813753762&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500', x: 12, y: 3, id: 45, isWin: false, createTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', updateTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', prizeName: [], prizeTime: [], prizeId: [] },
|
||||||
|
{ uid: '47', name: '黄桂球', department: '', identity: '', avatar: 'https://img1.baidu.com/it/u=2165937980,813753762&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500', x: 13, y: 3, id: 46, isWin: false, createTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', updateTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', prizeName: [], prizeTime: [], prizeId: [] },
|
||||||
|
{ uid: '48', name: '李霖淳', department: '', identity: '', avatar: 'https://img1.baidu.com/it/u=2165937980,813753762&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500', x: 14, y: 3, id: 47, isWin: false, createTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', updateTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', prizeName: [], prizeTime: [], prizeId: [] },
|
||||||
|
{ uid: '49', name: '时生亮', department: '', identity: '', avatar: 'https://img1.baidu.com/it/u=2165937980,813753762&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500', x: 15, y: 3, id: 48, isWin: false, createTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', updateTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', prizeName: [], prizeTime: [], prizeId: [] },
|
||||||
|
{ uid: '50', name: '童军林', department: '', identity: '', avatar: 'https://img1.baidu.com/it/u=2165937980,813753762&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500', x: 16, y: 3, id: 49, isWin: false, createTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', updateTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', prizeName: [], prizeTime: [], prizeId: [] },
|
||||||
|
{ uid: '51', name: '谯云', department: '', identity: '', avatar: 'https://img1.baidu.com/it/u=2165937980,813753762&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500', x: 17, y: 3, id: 50, isWin: false, createTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', updateTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', prizeName: [], prizeTime: [], prizeId: [] },
|
||||||
|
{ uid: '52', name: '赵连飞', department: '', identity: '', avatar: 'https://img1.baidu.com/it/u=2165937980,813753762&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500', x: 1, y: 4, id: 51, isWin: false, createTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', updateTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', prizeName: [], prizeTime: [], prizeId: [] },
|
||||||
|
{ uid: '53', name: '卢洪琼', department: '', identity: '', avatar: 'https://img1.baidu.com/it/u=2165937980,813753762&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500', x: 2, y: 4, id: 52, isWin: false, createTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', updateTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', prizeName: [], prizeTime: [], prizeId: [] },
|
||||||
|
{ uid: '54', name: '甘琪', department: '', identity: '', avatar: 'https://img1.baidu.com/it/u=2165937980,813753762&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500', x: 3, y: 4, id: 53, isWin: false, createTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', updateTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', prizeName: [], prizeTime: [], prizeId: [] },
|
||||||
|
{ uid: '55', name: '林盼', department: '', identity: '', avatar: 'https://img1.baidu.com/it/u=2165937980,813753762&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500', x: 4, y: 4, id: 54, isWin: false, createTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', updateTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', prizeName: [], prizeTime: [], prizeId: [] },
|
||||||
|
{ uid: '56', name: '杨凤娥', department: '', identity: '', avatar: 'https://img1.baidu.com/it/u=2165937980,813753762&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500', x: 5, y: 4, id: 55, isWin: false, createTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', updateTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', prizeName: [], prizeTime: [], prizeId: [] },
|
||||||
|
{ uid: '57', name: '于艺', department: '', identity: '', avatar: 'https://img1.baidu.com/it/u=2165937980,813753762&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500', x: 6, y: 4, id: 56, isWin: false, createTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', updateTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', prizeName: [], prizeTime: [], prizeId: [] },
|
||||||
|
{ uid: '58', name: '程鹏铨', department: '', identity: '', avatar: 'https://img1.baidu.com/it/u=2165937980,813753762&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500', x: 7, y: 4, id: 57, isWin: false, createTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', updateTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', prizeName: [], prizeTime: [], prizeId: [] },
|
||||||
|
{ uid: '59', name: '吴纬涛', department: '', identity: '', avatar: 'https://img1.baidu.com/it/u=2165937980,813753762&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500', x: 8, y: 4, id: 58, isWin: false, createTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', updateTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', prizeName: [], prizeTime: [], prizeId: [] },
|
||||||
|
{ uid: '60', name: '邹毅超', department: '', identity: '', avatar: 'https://img1.baidu.com/it/u=2165937980,813753762&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500', x: 9, y: 4, id: 59, isWin: false, createTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', updateTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', prizeName: [], prizeTime: [], prizeId: [] },
|
||||||
|
{ uid: '61', name: '魏山', department: '', identity: '', avatar: 'https://img1.baidu.com/it/u=2165937980,813753762&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500', x: 10, y: 4, id: 60, isWin: false, createTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', updateTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', prizeName: [], prizeTime: [], prizeId: [] },
|
||||||
|
{ uid: '62', name: '许诗琪', department: '', identity: '', avatar: 'https://img1.baidu.com/it/u=2165937980,813753762&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500', x: 11, y: 4, id: 61, isWin: false, createTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', updateTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', prizeName: [], prizeTime: [], prizeId: [] },
|
||||||
|
{ uid: '63', name: '罗日可', department: '', identity: '', avatar: 'https://img1.baidu.com/it/u=2165937980,813753762&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500', x: 12, y: 4, id: 62, isWin: false, createTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', updateTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', prizeName: [], prizeTime: [], prizeId: [] },
|
||||||
|
{ uid: '64', name: '王河北', department: '', identity: '', avatar: 'https://img1.baidu.com/it/u=2165937980,813753762&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500', x: 13, y: 4, id: 63, isWin: false, createTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', updateTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', prizeName: [], prizeTime: [], prizeId: [] },
|
||||||
|
{ uid: '65', name: '庞宝佳', department: '', identity: '', avatar: 'https://img1.baidu.com/it/u=2165937980,813753762&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500', x: 14, y: 4, id: 64, isWin: false, createTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', updateTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', prizeName: [], prizeTime: [], prizeId: [] },
|
||||||
|
{ uid: '66', name: '刘文炽', department: '', identity: '', avatar: 'https://img1.baidu.com/it/u=2165937980,813753762&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500', x: 15, y: 4, id: 65, isWin: false, createTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', updateTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', prizeName: [], prizeTime: [], prizeId: [] },
|
||||||
|
{ uid: '67', name: '董剑煜', department: '', identity: '', avatar: 'https://img1.baidu.com/it/u=2165937980,813753762&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500', x: 16, y: 4, id: 66, isWin: false, createTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', updateTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', prizeName: [], prizeTime: [], prizeId: [] },
|
||||||
|
{ uid: '68', name: '汤洁鸿', department: '', identity: '', avatar: 'https://img1.baidu.com/it/u=2165937980,813753762&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500', x: 17, y: 4, id: 67, isWin: false, createTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', updateTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', prizeName: [], prizeTime: [], prizeId: [] },
|
||||||
|
{ uid: '69', name: '徐星昊', department: '', identity: '', avatar: 'https://img1.baidu.com/it/u=2165937980,813753762&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500', x: 1, y: 5, id: 68, isWin: false, createTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', updateTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', prizeName: [], prizeTime: [], prizeId: [] },
|
||||||
|
{ uid: '70', name: '朱懿清', department: '', identity: '', avatar: 'https://img1.baidu.com/it/u=2165937980,813753762&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500', x: 2, y: 5, id: 69, isWin: false, createTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', updateTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', prizeName: [], prizeTime: [], prizeId: [] },
|
||||||
|
{ uid: '71', name: '王琳', department: '', identity: '', avatar: 'https://img1.baidu.com/it/u=2165937980,813753762&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500', x: 3, y: 5, id: 70, isWin: false, createTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', updateTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', prizeName: [], prizeTime: [], prizeId: [] },
|
||||||
|
{ uid: '72', name: '王冕', department: '', identity: '', avatar: 'https://img1.baidu.com/it/u=2165937980,813753762&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500', x: 4, y: 5, id: 71, isWin: false, createTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', updateTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', prizeName: [], prizeTime: [], prizeId: [] },
|
||||||
|
{ uid: '73', name: '刘岩', department: '', identity: '', avatar: 'https://img1.baidu.com/it/u=2165937980,813753762&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500', x: 5, y: 5, id: 72, isWin: false, createTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', updateTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', prizeName: [], prizeTime: [], prizeId: [] },
|
||||||
|
{ uid: '74', name: '许帆', department: '', identity: '', avatar: 'https://img1.baidu.com/it/u=2165937980,813753762&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500', x: 6, y: 5, id: 73, isWin: false, createTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', updateTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', prizeName: [], prizeTime: [], prizeId: [] },
|
||||||
|
{ uid: '75', name: '杨慧剑', department: '', identity: '', avatar: 'https://img1.baidu.com/it/u=2165937980,813753762&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500', x: 7, y: 5, id: 74, isWin: false, createTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', updateTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', prizeName: [], prizeTime: [], prizeId: [] },
|
||||||
|
{ uid: '76', name: '谭皓蓝', department: '', identity: '', avatar: 'https://img1.baidu.com/it/u=2165937980,813753762&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500', x: 8, y: 5, id: 75, isWin: false, createTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', updateTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', prizeName: [], prizeTime: [], prizeId: [] },
|
||||||
|
{ uid: '77', name: '朱巧', department: '', identity: '', avatar: 'https://img1.baidu.com/it/u=2165937980,813753762&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500', x: 9, y: 5, id: 76, isWin: false, createTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', updateTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', prizeName: [], prizeTime: [], prizeId: [] },
|
||||||
|
{ uid: '78', name: '冯烨', department: '', identity: '', avatar: 'https://img1.baidu.com/it/u=2165937980,813753762&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500', x: 10, y: 5, id: 77, isWin: false, createTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', updateTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', prizeName: [], prizeTime: [], prizeId: [] },
|
||||||
|
{ uid: '79', name: '李振', department: '', identity: '', avatar: 'https://img1.baidu.com/it/u=2165937980,813753762&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500', x: 11, y: 5, id: 78, isWin: false, createTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', updateTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', prizeName: [], prizeTime: [], prizeId: [] },
|
||||||
|
{ uid: '80', name: '徐云霆', department: '', identity: '', avatar: 'https://img1.baidu.com/it/u=2165937980,813753762&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500', x: 12, y: 5, id: 79, isWin: false, createTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', updateTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', prizeName: [], prizeTime: [], prizeId: [] },
|
||||||
|
{ uid: '81', name: '何文斌', department: '', identity: '', avatar: 'https://img1.baidu.com/it/u=2165937980,813753762&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500', x: 13, y: 5, id: 80, isWin: false, createTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', updateTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', prizeName: [], prizeTime: [], prizeId: [] },
|
||||||
|
{ uid: '82', name: '王雨昊', department: '', identity: '', avatar: 'https://img1.baidu.com/it/u=2165937980,813753762&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500', x: 14, y: 5, id: 81, isWin: false, createTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', updateTime: 'Tue Mar 10 2026 20:19:04 GMT+0800 (China Standard Time)', prizeName: [], prizeTime: [], prizeId: [] },
|
||||||
]
|
]
|
||||||
|
|
||||||
export const defaultMusicList = [
|
export const defaultMusicList = [
|
||||||
{
|
{
|
||||||
id: `Geoff Knorr - China (The Industrial Era).ogg${new Date().getTime().toString()}`,
|
id: `wake.mp3${new Date().getTime().toString()}`,
|
||||||
name: 'Geoff Knorr - China (The Industrial Era).ogg',
|
name: 'Wake (Live from Summercamp) - Hillsong Young & Free.mp3',
|
||||||
url: `${originUrl}/resource/audio/Geoff Knorr - China (The Industrial Era).ogg`,
|
url: '/log-lottery/wake.mp3',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
id: `Geoff Knorr&Phill Boucher - China (The Atomic Era).ogg${new Date().getTime().toString()}`,
|
|
||||||
name: 'Geoff Knorr&Phill Boucher - China (The Atomic Era).ogg',
|
|
||||||
url: `${originUrl}/resource/audio/Geoff Knorr&Phill Boucher - China (The Atomic Era).ogg`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: `Radetzky March.mp3${new Date().getTime().toString()}`,
|
|
||||||
name: 'Radetzky March.mp3',
|
|
||||||
url: `${originUrl}/resource/audio/Radetzky March.mp3`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: `Shanghai.mp3${new Date().getTime().toString()}`,
|
|
||||||
name: 'Shanghai.mp3',
|
|
||||||
url: `${originUrl}/resource/audio/Shanghai.mp3`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: `Waltz No.2.mp3${new Date().getTime().toString()}`,
|
|
||||||
name: 'Waltz No.2.mp3',
|
|
||||||
url: `${originUrl}/resource/audio/Waltz No.2.mp3`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: `WildChinaTheme.mp3${new Date().getTime().toString()}`,
|
|
||||||
name: 'WildChinaTheme.mp3',
|
|
||||||
url: `${originUrl}/resource/audio/WildChinaTheme.mp3`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: `边程&房东的猫 - 美好事物-再遇少年.ogg${new Date().getTime().toString()}`,
|
|
||||||
name: '边程&房东的猫 - 美好事物-再遇少年.ogg',
|
|
||||||
url: `${originUrl}/resource/audio/边程&房东的猫 - 美好事物-再遇少年.ogg`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: `大乔小乔 - 相见难别亦难.ogg${new Date().getTime().toString()}`,
|
|
||||||
name: '大乔小乔 - 相见难别亦难.ogg',
|
|
||||||
url: `${originUrl}/resource/audio/大乔小乔 - 相见难别亦难.ogg`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: `你要跳舞吗-新裤子.mp3${new Date().getTime().toString()}`,
|
|
||||||
name: '你要跳舞吗-新裤子.mp3',
|
|
||||||
url: `${originUrl}/resource/audio/你要跳舞吗-新裤子.mp3`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: `生命-声音玩具.mp3${new Date().getTime().toString()}`,
|
|
||||||
name: '生命-声音玩具.mp3',
|
|
||||||
url: `${originUrl}/resource/audio/生命-声音玩具.mp3`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: `与非门 - Happy New Year.ogg${new Date().getTime().toString()}`,
|
|
||||||
name: '与非门 - Happy New Year.ogg',
|
|
||||||
url: `${originUrl}/resource/audio/与非门 - Happy New Year.ogg`,
|
|
||||||
},
|
|
||||||
|
|
||||||
]
|
]
|
||||||
|
|
||||||
export const defaultPrizeList = <IPrizeConfig[]>[
|
export const defaultPrizeList = <IPrizeConfig[]>[
|
||||||
{
|
{
|
||||||
id: '001',
|
id: '001',
|
||||||
name: '三等奖',
|
name: '第一批次',
|
||||||
sort: 1,
|
sort: 1,
|
||||||
isAll: false,
|
isAll: false,
|
||||||
count: 3,
|
count: 5,
|
||||||
isUsedCount: 0,
|
isUsedCount: 0,
|
||||||
picture: {
|
picture: {
|
||||||
id: '2',
|
id: '1',
|
||||||
name: '三等奖',
|
name: '第一批次',
|
||||||
url: `${originUrl}/resource/image/image3.png`,
|
url: '/log-lottery/default-image.png',
|
||||||
},
|
},
|
||||||
separateCount: {
|
separateCount: {
|
||||||
enable: true,
|
enable: true,
|
||||||
countList: [],
|
countList: [],
|
||||||
},
|
},
|
||||||
desc: '三等奖',
|
desc: '第一批次',
|
||||||
isShow: true,
|
isShow: true,
|
||||||
isUsed: false,
|
isUsed: false,
|
||||||
frequency: 1,
|
frequency: 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '002',
|
id: '002',
|
||||||
name: '二等奖',
|
name: '第二批次',
|
||||||
sort: 2,
|
sort: 2,
|
||||||
isAll: false,
|
isAll: false,
|
||||||
count: 2,
|
count: 5,
|
||||||
isUsedCount: 0,
|
isUsedCount: 0,
|
||||||
picture: {
|
picture: {
|
||||||
id: '1',
|
id: '2',
|
||||||
name: '二等奖',
|
name: '第二批次',
|
||||||
url: `${originUrl}/resource/image/image2.png`,
|
url: '/log-lottery/default-image.png',
|
||||||
},
|
},
|
||||||
separateCount: {
|
separateCount: {
|
||||||
enable: false,
|
enable: true,
|
||||||
countList: [],
|
countList: [],
|
||||||
},
|
},
|
||||||
desc: '二等奖',
|
desc: '第二批次',
|
||||||
isShow: true,
|
|
||||||
isUsed: false,
|
|
||||||
frequency: 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '003',
|
|
||||||
name: '一等奖',
|
|
||||||
sort: 3,
|
|
||||||
isAll: false,
|
|
||||||
count: 1,
|
|
||||||
isUsedCount: 0,
|
|
||||||
picture: {
|
|
||||||
id: '0',
|
|
||||||
name: '一等奖',
|
|
||||||
url: `${originUrl}/resource/image/image1.png`,
|
|
||||||
},
|
|
||||||
separateCount: {
|
|
||||||
enable: false,
|
|
||||||
countList: [],
|
|
||||||
},
|
|
||||||
desc: '一等奖',
|
|
||||||
isShow: true,
|
|
||||||
isUsed: false,
|
|
||||||
frequency: 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '004',
|
|
||||||
name: '超级大奖',
|
|
||||||
sort: 4,
|
|
||||||
isAll: false,
|
|
||||||
count: 1,
|
|
||||||
isUsedCount: 0,
|
|
||||||
picture: {
|
|
||||||
id: '3',
|
|
||||||
name: '超级奖',
|
|
||||||
url: `${originUrl}/resource/image/image4.png`,
|
|
||||||
},
|
|
||||||
separateCount: {
|
|
||||||
enable: false,
|
|
||||||
countList: [],
|
|
||||||
},
|
|
||||||
desc: '超级大奖',
|
|
||||||
isShow: true,
|
|
||||||
isUsed: false,
|
|
||||||
frequency: 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '005',
|
|
||||||
name: '特别奖',
|
|
||||||
sort: 5,
|
|
||||||
isAll: false,
|
|
||||||
count: 1,
|
|
||||||
isUsedCount: 0,
|
|
||||||
picture: {
|
|
||||||
id: '4',
|
|
||||||
name: '特别奖',
|
|
||||||
url: `${originUrl}/resource/image/image5.png`,
|
|
||||||
},
|
|
||||||
separateCount: {
|
|
||||||
enable: false,
|
|
||||||
countList: [],
|
|
||||||
},
|
|
||||||
desc: '特别奖',
|
|
||||||
isShow: true,
|
isShow: true,
|
||||||
isUsed: false,
|
isUsed: false,
|
||||||
frequency: 1,
|
frequency: 1,
|
||||||
@@ -219,7 +151,7 @@ export const defaultCurrentPrize = <IPrizeConfig>{
|
|||||||
picture: {
|
picture: {
|
||||||
id: '2',
|
id: '2',
|
||||||
name: '三等奖',
|
name: '三等奖',
|
||||||
url: `${originUrl}/resource/image/image3.png`,
|
url: '/log-lottery/default-image.png',
|
||||||
},
|
},
|
||||||
separateCount: {
|
separateCount: {
|
||||||
enable: true,
|
enable: true,
|
||||||
@@ -256,30 +188,49 @@ export const defaultImageList = [
|
|||||||
{
|
{
|
||||||
id: '0',
|
id: '0',
|
||||||
name: '一等奖',
|
name: '一等奖',
|
||||||
url: `${originUrl}/resource/image/image1.png`,
|
url: '/log-lottery/default-image.png',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '1',
|
id: '1',
|
||||||
name: '二等奖',
|
name: '二等奖',
|
||||||
url: `${originUrl}/resource/image/image2.png`,
|
url: '/log-lottery/default-image.png',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '2',
|
id: '2',
|
||||||
name: '三等奖',
|
name: '三等奖',
|
||||||
url: `${originUrl}/resource/image/image3.png`,
|
url: '/log-lottery/default-image.png',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '3',
|
id: '3',
|
||||||
name: '超级奖',
|
name: '超级奖',
|
||||||
url: `${originUrl}/resource/image/image4.png`,
|
url: '/log-lottery/default-image.png',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '4',
|
id: '4',
|
||||||
name: '特别奖',
|
name: '特别奖',
|
||||||
url: `${originUrl}/resource/image/image5.png`,
|
url: '/log-lottery/default-image.png',
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
export const defaultPatternList = [21, 38, 55, 54, 53, 70, 87, 88, 89, 23, 40, 57, 74, 91, 92, 76, 59, 42, 25, 24, 27, 28, 29, 46, 63, 62, 61, 78, 95, 96, 97, 20, 19, 31, 48, 66, 67, 84, 101, 100, 32, 33, 93, 65, 82, 99]
|
// Pattern for "L N" - 17 columns x 7 rows grid
|
||||||
|
// L starts at column 3 (with 2 empty columns before)
|
||||||
|
// N starts at column 8 (with 2 empty columns between L and N)
|
||||||
|
// Row 1: column 3 (L), columns 8-9 (N)
|
||||||
|
// Row 2: column 3 (L), columns 8-10 (N)
|
||||||
|
// Row 3: column 3 (L), columns 8-11 (N)
|
||||||
|
// Row 4: column 3 (L), columns 8,10-12 (N)
|
||||||
|
// Row 5: column 3 (L), columns 8,11-13 (N)
|
||||||
|
// Row 6: column 3 (L), columns 8,12-13 (N)
|
||||||
|
// Row 7: columns 3-7 (L), columns 8,13 (N)
|
||||||
|
export const defaultPatternList = [
|
||||||
|
4, 10, 15,
|
||||||
|
21, 27, 28, 32,
|
||||||
|
38, 44, 45, 46, 49,
|
||||||
|
55, 61, 63, 64, 66,
|
||||||
|
72, 78, 81, 82, 83,
|
||||||
|
89, 95, 99, 100,
|
||||||
|
106, 107, 108, 109, 110, 112,
|
||||||
|
117
|
||||||
|
]
|
||||||
|
|
||||||
export const defaultServerHostList = [
|
export const defaultServerHostList = [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -15,15 +15,15 @@ export const useGlobalConfig = defineStore('global', {
|
|||||||
definiteTime: null as number | null,
|
definiteTime: null as number | null,
|
||||||
winMusic: false,
|
winMusic: false,
|
||||||
theme: {
|
theme: {
|
||||||
name: 'dracula',
|
name: 'cmyk',
|
||||||
detail: { primary: '#0f5fd3' },
|
detail: { primary: '#4cb050' },
|
||||||
cardColor: '#ff79c6',
|
cardColor: '#026941',
|
||||||
cardWidth: 140,
|
cardWidth: 140,
|
||||||
cardHeight: 200,
|
cardHeight: 200,
|
||||||
textColor: '#00000000',
|
textColor: '#00000000',
|
||||||
luckyCardColor: '#ECB1AC',
|
luckyCardColor: '#026941',
|
||||||
textSize: 30,
|
textSize: 30,
|
||||||
patternColor: '#1b66c9',
|
patternColor: '#2c2c2c',
|
||||||
patternList: defaultPatternList as number[],
|
patternList: defaultPatternList as number[],
|
||||||
background: {}, // 背景颜色或图片
|
background: {}, // 背景颜色或图片
|
||||||
font: '微软雅黑',
|
font: '微软雅黑',
|
||||||
@@ -295,15 +295,15 @@ export const useGlobalConfig = defineStore('global', {
|
|||||||
language: browserLanguage,
|
language: browserLanguage,
|
||||||
definiteTime: null,
|
definiteTime: null,
|
||||||
theme: {
|
theme: {
|
||||||
name: 'dracula',
|
name: 'cmyk',
|
||||||
detail: { primary: '#0f5fd3' },
|
detail: { primary: '#4cb050' },
|
||||||
cardColor: '#ff79c6',
|
cardColor: '#026941',
|
||||||
cardWidth: 140,
|
cardWidth: 140,
|
||||||
cardHeight: 200,
|
cardHeight: 200,
|
||||||
textColor: '#00000000',
|
textColor: '#00000000',
|
||||||
luckyCardColor: '#ECB1AC',
|
luckyCardColor: '#026941',
|
||||||
textSize: 30,
|
textSize: 30,
|
||||||
patternColor: '#1b66c9',
|
patternColor: '#2c2c2c',
|
||||||
patternList: defaultPatternList as number[],
|
patternList: defaultPatternList as number[],
|
||||||
background: {}, // 背景颜色或图片
|
background: {}, // 背景颜色或图片
|
||||||
font: '微软雅黑',
|
font: '微软雅黑',
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useGlobalConfig } from './globalConfig'
|
import { useGlobalConfig } from './globalConfig'
|
||||||
import { usePersonConfig } from './personConfig'
|
import { usePersonConfig } from './personConfig'
|
||||||
import { usePrizeConfig } from './prizeConfig'
|
import { usePrizeConfig } from './prizeConfig'
|
||||||
|
import { usePrizeDrawStore } from './prizeDrawConfig'
|
||||||
import { useServerConfig } from './serverConfig'
|
import { useServerConfig } from './serverConfig'
|
||||||
import { useSystem } from './system'
|
import { useSystem } from './system'
|
||||||
|
|
||||||
@@ -11,5 +12,6 @@ export default function useStore() {
|
|||||||
globalConfig: useGlobalConfig(),
|
globalConfig: useGlobalConfig(),
|
||||||
system: useSystem(),
|
system: useSystem(),
|
||||||
serverConfig: useServerConfig(),
|
serverConfig: useServerConfig(),
|
||||||
|
prizeDrawStore: usePrizeDrawStore(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
321
src/store/prizeDrawConfig.ts
Normal 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,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -111,4 +111,4 @@
|
|||||||
height: 240px !important;
|
height: 240px !important;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,22 +40,26 @@ const { t } = useI18n()
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="absolute z-10 flex flex-col items-center justify-center -translate-x-1/2 left-1/2">
|
<div class="absolute z-10 flex flex-col items-center justify-center -translate-x-1/2 left-1/2">
|
||||||
<h2
|
<div class="lingniu-title-row pt-12 mb-12">
|
||||||
class="pt-12 m-0 mb-12 tracking-wide text-center leading-12"
|
<span v-if="!isTextColor" class="title-ornament title-ornament--left" aria-hidden="true" />
|
||||||
:class="{ 'animate-pulse bg-linear-to-r from-primary via-secondary to-accent bg-clip-text text-transparent': !isTextColor }"
|
<h2
|
||||||
:style="titleStyle"
|
class="lingniu-title-text m-0 text-center"
|
||||||
>
|
:class="{ 'lingniu-title-gradient': !isTextColor }"
|
||||||
{{ topTitle }}
|
:style="titleStyle"
|
||||||
</h2>
|
>
|
||||||
|
{{ topTitle }}
|
||||||
|
</h2>
|
||||||
|
<span v-if="!isTextColor" class="title-ornament title-ornament--right" aria-hidden="true" />
|
||||||
|
</div>
|
||||||
<div v-if="isInitialDone" class="flex gap-3">
|
<div v-if="isInitialDone" class="flex gap-3">
|
||||||
<button
|
<button
|
||||||
v-if="tableData.length <= 0" class="cursor-pointer btn btn-outline btn-secondary btn-lg"
|
v-if="tableData.length <= 0" class="cursor-pointer btn btn-outline btn-lg lingniu-btn"
|
||||||
@click="router.push('config')"
|
@click="router.push('config')"
|
||||||
>
|
>
|
||||||
{{ t('button.noInfoAndImport') }}
|
{{ t('button.noInfoAndImport') }}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
v-if="tableData.length <= 0" class="cursor-pointer btn btn-outline btn-secondary btn-lg"
|
v-if="tableData.length <= 0" class="cursor-pointer btn btn-outline btn-lg lingniu-btn"
|
||||||
@click="setDefaultPersonList"
|
@click="setDefaultPersonList"
|
||||||
>
|
>
|
||||||
{{ t('button.useDefault') }}
|
{{ t('button.useDefault') }}
|
||||||
@@ -70,11 +74,132 @@ const { t } = useI18n()
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.header-title {
|
.lingniu-title-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
gap: 28px;
|
||||||
|
white-space: nowrap;
|
||||||
-webkit-animation: tracking-in-expand-fwd 0.8s cubic-bezier(0.215, 0.610, 0.355, 1.000) both;
|
-webkit-animation: tracking-in-expand-fwd 0.8s cubic-bezier(0.215, 0.610, 0.355, 1.000) both;
|
||||||
animation: tracking-in-expand-fwd 0.8s cubic-bezier(0.215, 0.610, 0.355, 1.000) both;
|
animation: tracking-in-expand-fwd 0.8s cubic-bezier(0.215, 0.610, 0.355, 1.000) both;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.lingniu-title-text {
|
||||||
|
line-height: 1.2;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.22em;
|
||||||
|
padding-inline: 0.22em; // 防止letter-spacing截断尾字渐变
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lingniu-title-gradient {
|
||||||
|
background: linear-gradient(
|
||||||
|
110deg,
|
||||||
|
#2F2828 0%,
|
||||||
|
#007143 18%,
|
||||||
|
#12a86b 36%,
|
||||||
|
#f5d27a 50%,
|
||||||
|
#12a86b 64%,
|
||||||
|
#007143 82%,
|
||||||
|
#2F2828 100%
|
||||||
|
);
|
||||||
|
background-size: 220% 100%;
|
||||||
|
background-clip: text;
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
color: transparent;
|
||||||
|
filter: drop-shadow(0 6px 24px rgba(0, 113, 67, 0.35));
|
||||||
|
animation: lingniu-shine 6s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-ornament {
|
||||||
|
--ornament-w: clamp(56px, 12vw, 140px);
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
width: var(--ornament-w);
|
||||||
|
height: 1px;
|
||||||
|
background: linear-gradient(to right, transparent 0%, rgba(0, 113, 67, 0.05) 10%, #007143 50%, rgba(0, 113, 67, 0.05) 90%, transparent 100%);
|
||||||
|
flex: none;
|
||||||
|
|
||||||
|
&::before,
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
transform: translateY(-50%) rotate(45deg);
|
||||||
|
background: linear-gradient(135deg, #f5d27a 0%, #d4a960 100%);
|
||||||
|
box-shadow:
|
||||||
|
0 0 14px rgba(0, 113, 67, 0.55),
|
||||||
|
0 0 4px rgba(245, 210, 122, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
&--left {
|
||||||
|
&::before {
|
||||||
|
left: 50%;
|
||||||
|
margin-left: -4px;
|
||||||
|
background: linear-gradient(135deg, #007143 0%, #12a86b 100%);
|
||||||
|
}
|
||||||
|
&::after {
|
||||||
|
right: -4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&--right {
|
||||||
|
&::before {
|
||||||
|
left: -4px;
|
||||||
|
}
|
||||||
|
&::after {
|
||||||
|
right: 50%;
|
||||||
|
margin-right: -4px;
|
||||||
|
background: linear-gradient(135deg, #007143 0%, #12a86b 100%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.lingniu-btn {
|
||||||
|
--btn-color: #007143;
|
||||||
|
--btn-color-hover: #12a86b;
|
||||||
|
color: #fff;
|
||||||
|
border-color: rgba(0, 113, 67, 0.6);
|
||||||
|
background: rgba(0, 113, 67, 0.08);
|
||||||
|
backdrop-filter: blur(6px);
|
||||||
|
box-shadow:
|
||||||
|
0 4px 18px rgba(0, 113, 67, 0.18),
|
||||||
|
inset 0 0 0 1px rgba(245, 210, 122, 0.12);
|
||||||
|
transition: transform 0.25s ease, box-shadow 0.25s ease, background 0.25s ease, border-color 0.25s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: linear-gradient(135deg, rgba(0, 113, 67, 0.85), rgba(18, 168, 107, 0.85));
|
||||||
|
border-color: rgba(245, 210, 122, 0.7);
|
||||||
|
box-shadow:
|
||||||
|
0 10px 28px rgba(0, 113, 67, 0.45),
|
||||||
|
inset 0 0 0 1px rgba(245, 210, 122, 0.45);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.lingniu-title-row {
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
.title-ornament {
|
||||||
|
--ornament-w: 36px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes lingniu-shine {
|
||||||
|
0% {
|
||||||
|
background-position: 200% 50%;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
background-position: -200% 50%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@-webkit-keyframes tracking-in-expand-fwd {
|
@-webkit-keyframes tracking-in-expand-fwd {
|
||||||
0% {
|
0% {
|
||||||
letter-spacing: -0.5em;
|
letter-spacing: -0.5em;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { storeToRefs } from 'pinia'
|
import { storeToRefs } from 'pinia'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
import useStore from '@/store'
|
import useStore from '@/store'
|
||||||
import HeaderTitle from './components/HeaderTitle/index.vue'
|
import HeaderTitle from './components/HeaderTitle/index.vue'
|
||||||
import OptionButton from './components/OptionsButton/index.vue'
|
import OptionButton from './components/OptionsButton/index.vue'
|
||||||
@@ -11,8 +12,13 @@ import 'vue-toast-notification/dist/theme-sugar.css'
|
|||||||
const viewModel = useViewModel()
|
const viewModel = useViewModel()
|
||||||
const { setDefaultPersonList, tableData, currentStatus, enterLottery, stopLottery, containerRef, startLottery, continueLottery, quitLottery, isInitialDone, titleFont, titleFontSyncGlobal } = viewModel
|
const { setDefaultPersonList, tableData, currentStatus, enterLottery, stopLottery, containerRef, startLottery, continueLottery, quitLottery, isInitialDone, titleFont, titleFontSyncGlobal } = viewModel
|
||||||
const globalConfig = useStore().globalConfig
|
const globalConfig = useStore().globalConfig
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
const { getTopTitle: topTitle, getTextColor: textColor, getTextSize: textSize, getBackground: homeBackground } = storeToRefs(globalConfig)
|
const { getTopTitle: topTitle, getTextColor: textColor, getTextSize: textSize, getBackground: homeBackground } = storeToRefs(globalConfig)
|
||||||
|
|
||||||
|
function openPrizeDraw() {
|
||||||
|
window.open('/log-lottery/prize-draw', '_blank')
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -39,6 +45,16 @@ const { getTopTitle: topTitle, getTextColor: textColor, getTextSize: textSize, g
|
|||||||
</div>
|
</div>
|
||||||
<StarsBackground :home-background="homeBackground" />
|
<StarsBackground :home-background="homeBackground" />
|
||||||
<PrizeList class="absolute left-0 top-32" />
|
<PrizeList class="absolute left-0 top-32" />
|
||||||
|
|
||||||
|
<!-- 右下角进入抽奖按钮 -->
|
||||||
|
<div v-if="isInitialDone" class="fixed bottom-8 right-8 z-[9999] pointer-events-auto">
|
||||||
|
<button
|
||||||
|
class="cursor-pointer btn btn-outline btn-primary btn-lg shadow-lg hover:shadow-xl transition-all px-6 py-4"
|
||||||
|
@click="openPrizeDraw"
|
||||||
|
>
|
||||||
|
🎁 进入活动抽奖
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
|
|||||||
@@ -8,9 +8,7 @@ import { CSS3DObject, CSS3DRenderer } from 'three-css3d'
|
|||||||
import { TrackballControls } from 'three/examples/jsm/controls/TrackballControls.js'
|
import { TrackballControls } from 'three/examples/jsm/controls/TrackballControls.js'
|
||||||
import { nextTick, onMounted, onUnmounted, ref } from 'vue'
|
import { nextTick, onMounted, onUnmounted, ref } from 'vue'
|
||||||
import { useToast } from 'vue-toast-notification'
|
import { useToast } from 'vue-toast-notification'
|
||||||
import dongSound from '@/assets/audio/end.mp3'
|
import worldCupAudio from '@/assets/audio/worldcup.mp3?url'
|
||||||
import enterAudio from '@/assets/audio/enter.wav'
|
|
||||||
import worldCupAudio from '@/assets/audio/worldcup.mp3'
|
|
||||||
import { CONFETTI_FIRE_MAX_COUNT, SINGLE_TIME_MAX_PERSON_COUNT } from '@/constant/config'
|
import { CONFETTI_FIRE_MAX_COUNT, SINGLE_TIME_MAX_PERSON_COUNT } from '@/constant/config'
|
||||||
import { useElementPosition, useElementStyle } from '@/hooks/useElement'
|
import { useElementPosition, useElementStyle } from '@/hooks/useElement'
|
||||||
import i18n from '@/locales/i18n'
|
import i18n from '@/locales/i18n'
|
||||||
@@ -357,9 +355,20 @@ export function useViewModel() {
|
|||||||
lotteryMusic.value = new Audio(worldCupAudio)
|
lotteryMusic.value = new Audio(worldCupAudio)
|
||||||
lotteryMusic.value.loop = true
|
lotteryMusic.value.loop = true
|
||||||
lotteryMusic.value.volume = 0.7
|
lotteryMusic.value.volume = 0.7
|
||||||
|
|
||||||
|
// 添加音频加载事件监听
|
||||||
|
lotteryMusic.value.addEventListener('canplaythrough', () => {
|
||||||
|
console.log('音频加载完成')
|
||||||
|
})
|
||||||
|
|
||||||
|
lotteryMusic.value.addEventListener('error', (e) => {
|
||||||
|
console.error('音频加载错误:', e)
|
||||||
|
console.error('音频路径:', worldCupAudio)
|
||||||
|
})
|
||||||
|
|
||||||
lotteryMusic.value.play().catch((error) => {
|
lotteryMusic.value.play().catch((error) => {
|
||||||
console.error('播放抽奖音乐失败:', error)
|
console.error('播放抽奖音乐失败:', error)
|
||||||
|
console.error('音频路径:', worldCupAudio)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -380,45 +389,7 @@ export function useViewModel() {
|
|||||||
* @description: 播放结束音效
|
* @description: 播放结束音效
|
||||||
*/
|
*/
|
||||||
function playEndSound() {
|
function playEndSound() {
|
||||||
if (!isPlayWinMusic.value) {
|
// 已移除结束音效
|
||||||
return
|
|
||||||
}
|
|
||||||
console.log('准备播放结束音效', dongSound)
|
|
||||||
|
|
||||||
// 清理已结束的音频
|
|
||||||
playingAudios.value = playingAudios.value.filter(audio => !audio.ended)
|
|
||||||
|
|
||||||
try {
|
|
||||||
const endSound = new Audio(dongSound)
|
|
||||||
endSound.volume = 1.0
|
|
||||||
|
|
||||||
// 简化播放逻辑
|
|
||||||
const playPromise = endSound.play()
|
|
||||||
|
|
||||||
if (playPromise) {
|
|
||||||
playPromise
|
|
||||||
.then(() => {
|
|
||||||
console.log('结束音效播放成功')
|
|
||||||
playingAudios.value.push(endSound)
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
console.error('播放失败:', err.name, err.message)
|
|
||||||
if (err.name === 'NotAllowedError') {
|
|
||||||
console.warn('自动播放被阻止,需用户交互后播放')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
endSound.onended = () => {
|
|
||||||
console.log('结束音效播放完成')
|
|
||||||
const index = playingAudios.value.indexOf(endSound)
|
|
||||||
if (index > -1)
|
|
||||||
playingAudios.value.splice(index, 1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (error) {
|
|
||||||
console.error('创建音频对象失败:', error)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -470,7 +441,7 @@ export function useViewModel() {
|
|||||||
if (patternList.value.length) {
|
if (patternList.value.length) {
|
||||||
for (let i = 0; i < patternList.value.length; i++) {
|
for (let i = 0; i < patternList.value.length; i++) {
|
||||||
if (i < rowCount.value * 7) {
|
if (i < rowCount.value * 7) {
|
||||||
objects.value[patternList.value[i] - 1].element.style.backgroundColor = rgba(cardColor.value, Math.random() * 0.5 + 0.25)
|
objects.value[patternList.value[i] - 1].element.style.backgroundColor = rgba(patternColor.value, Math.random() * 0.2 + 0.8)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -630,47 +601,7 @@ export function useViewModel() {
|
|||||||
}
|
}
|
||||||
// 播放音频,中将卡片越多audio对象越多,声音越大
|
// 播放音频,中将卡片越多audio对象越多,声音越大
|
||||||
function playWinMusic() {
|
function playWinMusic() {
|
||||||
if (!isPlayWinMusic.value) {
|
// 已移除中奖音效
|
||||||
return
|
|
||||||
}
|
|
||||||
// 清理已结束的音频
|
|
||||||
playingAudios.value = playingAudios.value.filter(audio => !audio.ended && !audio.paused)
|
|
||||||
|
|
||||||
if (playingAudios.value.length > maxAudioLimit) {
|
|
||||||
console.log('音频播放数量已达到上限,请勿重复播放')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const enterNewAudio = new Audio(enterAudio)
|
|
||||||
enterNewAudio.volume = 0.8
|
|
||||||
|
|
||||||
playingAudios.value.push(enterNewAudio)
|
|
||||||
enterNewAudio.play()
|
|
||||||
.then(() => {
|
|
||||||
// 当音频播放结束后,从数组中移除
|
|
||||||
enterNewAudio.onended = () => {
|
|
||||||
const index = playingAudios.value.indexOf(enterNewAudio)
|
|
||||||
if (index > -1) {
|
|
||||||
playingAudios.value.splice(index, 1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.error('播放音频失败:', error)
|
|
||||||
// 如果播放失败,也从数组中移除
|
|
||||||
const index = playingAudios.value.indexOf(enterNewAudio)
|
|
||||||
if (index > -1) {
|
|
||||||
playingAudios.value.splice(index, 1)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// 播放错误时从数组中移除
|
|
||||||
enterNewAudio.onerror = () => {
|
|
||||||
const index = playingAudios.value.indexOf(enterNewAudio)
|
|
||||||
if (index > -1) {
|
|
||||||
playingAudios.value.splice(index, 1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* @description: 继续,意味着这抽奖作数,计入数据库
|
* @description: 继续,意味着这抽奖作数,计入数据库
|
||||||
|
|||||||
548
src/views/PrizeDraw/components/DrawArea.vue
Normal file
@@ -0,0 +1,548 @@
|
|||||||
|
<template>
|
||||||
|
<div class="draw-area">
|
||||||
|
<!-- 初始状态 -->
|
||||||
|
<div v-if="!lastResult && !isDrawing && !hasStarted" class="draw-initial">
|
||||||
|
<div class="draw-icon">🎁</div>
|
||||||
|
<h2 class="draw-title">准备抽奖</h2>
|
||||||
|
<p class="draw-subtitle">点击下方按钮抽取您的奖品</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 奖品卡片墙 - 使用原页面卡片样式 -->
|
||||||
|
<div v-else class="prize-wall-container">
|
||||||
|
<div class="prize-wall">
|
||||||
|
<div
|
||||||
|
v-for="(prize, index) in prizeCards"
|
||||||
|
:key="index"
|
||||||
|
class="element-card"
|
||||||
|
:class="{
|
||||||
|
'card-drawn': prize.isDrawn,
|
||||||
|
'card-highlight': isDrawing && index === highlightIndex,
|
||||||
|
'card-winner': !isDrawing && lastResult && index === winnerCardIndex
|
||||||
|
}"
|
||||||
|
:style="getCardStyle(prize, index)"
|
||||||
|
>
|
||||||
|
<div class="card-uid">{{ prize.isDrawn ? prize.name.substring(0, 4) : '?' }}</div>
|
||||||
|
<div class="card-name">{{ prize.isDrawn ? getPrizeIcon(prize.name) : '?' }}</div>
|
||||||
|
<div class="card-info">{{ prize.isDrawn ? prize.description.substring(0, 8) : '神秘奖品' }}</div>
|
||||||
|
<img v-if="prize.isDrawn" class="card-avatar" :src="getPrizeImage(prize.name)" alt="">
|
||||||
|
<div v-else class="card-mystery">?</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<canvas ref="confettiCanvas" class="confetti-canvas" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 抽奖按钮 -->
|
||||||
|
<div class="draw-actions">
|
||||||
|
<button
|
||||||
|
v-if="!isCompleted"
|
||||||
|
class="btn-draw"
|
||||||
|
:disabled="isDrawing"
|
||||||
|
@click="handleDraw"
|
||||||
|
>
|
||||||
|
<span v-if="!hasStarted">开始抽奖</span>
|
||||||
|
<span v-else>继续抽奖</span>
|
||||||
|
</button>
|
||||||
|
<div v-else class="completed-message">
|
||||||
|
<div class="completed-icon">🎉</div>
|
||||||
|
<h3>抽奖已完成!</h3>
|
||||||
|
<p>所有88位员工都已抽到奖品</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, nextTick, onMounted, ref, watch } from 'vue'
|
||||||
|
import confetti from 'canvas-confetti'
|
||||||
|
import type { DrawResult, PrizeConfig } from '@/store/prizeDrawConfig'
|
||||||
|
import { usePrizeDrawStore } from '@/store/prizeDrawConfig'
|
||||||
|
import { rgba } from '@/utils/color'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
isDrawing: boolean
|
||||||
|
isCompleted: boolean
|
||||||
|
lastResult?: DrawResult
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
draw: []
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const store = usePrizeDrawStore()
|
||||||
|
const confettiCanvas = ref<HTMLCanvasElement>()
|
||||||
|
const highlightIndex = ref(0)
|
||||||
|
const hasStarted = ref(false)
|
||||||
|
const winnerCardIndex = ref(-1)
|
||||||
|
|
||||||
|
// 初始化时的随机顺序(保持不变)
|
||||||
|
const initialCardOrder = ref<Array<PrizeConfig & { isDrawn: boolean, uniqueId: string }>>([])
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
// 只在第一次初始化时生成随机顺序
|
||||||
|
if (initialCardOrder.value.length === 0) {
|
||||||
|
const cards: Array<PrizeConfig & { isDrawn: boolean, uniqueId: string }> = []
|
||||||
|
|
||||||
|
store.prizeConfigs.forEach((config) => {
|
||||||
|
for (let i = 0; i < config.totalCount; i++) {
|
||||||
|
cards.push({
|
||||||
|
...config,
|
||||||
|
isDrawn: false,
|
||||||
|
uniqueId: `${config.id}-${i}-${Math.random()}`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 使用 Fisher-Yates 洗牌算法随机打乱
|
||||||
|
for (let i = cards.length - 1; i > 0; i--) {
|
||||||
|
const j = Math.floor(Math.random() * (i + 1));
|
||||||
|
[cards[i], cards[j]] = [cards[j], cards[i]]
|
||||||
|
}
|
||||||
|
|
||||||
|
initialCardOrder.value = cards
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 生成88个奖品卡片(包含已抽和未抽状态)
|
||||||
|
const prizeCards = computed(() => {
|
||||||
|
if (initialCardOrder.value.length === 0)
|
||||||
|
return []
|
||||||
|
|
||||||
|
// 根据抽奖结果更新卡片状态
|
||||||
|
return initialCardOrder.value.map((card) => {
|
||||||
|
// 检查这个奖品是否已被抽中
|
||||||
|
const config = store.prizeConfigs.find(c => c.id === card.id)
|
||||||
|
if (!config)
|
||||||
|
return card
|
||||||
|
|
||||||
|
const drawnCount = config.totalCount - config.remainingCount
|
||||||
|
|
||||||
|
// 计算这张卡片在同类奖品中的序号
|
||||||
|
const sameTypeCards = initialCardOrder.value.filter(c => c.id === card.id)
|
||||||
|
const cardIndex = sameTypeCards.findIndex(c => c.uniqueId === card.uniqueId)
|
||||||
|
|
||||||
|
// 如果这张卡片的序号小于已抽数量,则标记为已抽
|
||||||
|
return {
|
||||||
|
...card,
|
||||||
|
isDrawn: cardIndex < drawnCount,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// 获取卡片样式(模仿原页面)
|
||||||
|
function getCardStyle(prize: PrizeConfig & { isDrawn: boolean }, index: number) {
|
||||||
|
const baseColor = prize.color
|
||||||
|
const isHighlight = props.isDrawing && index === highlightIndex.value
|
||||||
|
const isWinner = !props.isDrawing && props.lastResult && index === winnerCardIndex.value
|
||||||
|
const isDrawn = prize.isDrawn
|
||||||
|
|
||||||
|
let bgColor = rgba(baseColor, Math.random() * 0.5 + 0.25)
|
||||||
|
let borderColor = rgba(baseColor, 0.25)
|
||||||
|
let boxShadow = `0 0 12px ${rgba(baseColor, 0.5)}`
|
||||||
|
|
||||||
|
if (isDrawn) {
|
||||||
|
bgColor = rgba('#95a5a6', 0.3)
|
||||||
|
borderColor = rgba('#95a5a6', 0.2)
|
||||||
|
boxShadow = `0 0 8px ${rgba('#95a5a6', 0.3)}`
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isHighlight) {
|
||||||
|
borderColor = rgba('#f39c12', 0.9)
|
||||||
|
boxShadow = `0 0 30px ${rgba('#f39c12', 0.8)}, 0 0 60px ${rgba('#f39c12', 0.4)}`
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isWinner) {
|
||||||
|
bgColor = rgba(baseColor, 0.9)
|
||||||
|
borderColor = rgba('#27ae60', 0.9)
|
||||||
|
boxShadow = `0 0 40px ${rgba('#27ae60', 0.8)}, 0 0 80px ${rgba('#27ae60', 0.4)}`
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
backgroundColor: bgColor,
|
||||||
|
border: `1px solid ${borderColor}`,
|
||||||
|
boxShadow,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPrizeIcon(name: string): string {
|
||||||
|
const iconMap: Record<string, string> = {
|
||||||
|
'快乐通勤奖': '⏰',
|
||||||
|
'跑马场自由日': '🏠',
|
||||||
|
'前途光明奖': '💼',
|
||||||
|
'现金红包500元': '💰',
|
||||||
|
'现金红包300元': '💵',
|
||||||
|
'现金红包200元': '💴',
|
||||||
|
'现金红包100元': '💸',
|
||||||
|
}
|
||||||
|
return iconMap[name] || '🎁'
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPrizeImage(name: string): string {
|
||||||
|
// 返回默认图片或根据奖品类型返回不同图片
|
||||||
|
return 'https://img1.baidu.com/it/u=2165937980,813753762&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听抽奖状态
|
||||||
|
watch(() => props.isDrawing, (isDrawing) => {
|
||||||
|
if (isDrawing) {
|
||||||
|
hasStarted.value = true
|
||||||
|
startScrollAnimation()
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
stopScrollAnimation()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 监听结果变化
|
||||||
|
watch(() => props.lastResult, (result) => {
|
||||||
|
if (result) {
|
||||||
|
nextTick(() => {
|
||||||
|
fireConfetti()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
let scrollInterval: number | null = null
|
||||||
|
|
||||||
|
function startScrollAnimation() {
|
||||||
|
// 随机滚动时长 5-8秒
|
||||||
|
const duration = 5000 + Math.random() * 3000
|
||||||
|
const startTime = Date.now()
|
||||||
|
let currentIndex = 0
|
||||||
|
|
||||||
|
// 只在未抽的卡片中滚动
|
||||||
|
const availableIndices = prizeCards.value
|
||||||
|
.map((card, index) => ({ card, index }))
|
||||||
|
.filter(item => !item.card.isDrawn)
|
||||||
|
.map(item => item.index)
|
||||||
|
|
||||||
|
if (availableIndices.length === 0)
|
||||||
|
return
|
||||||
|
|
||||||
|
scrollInterval = window.setInterval(() => {
|
||||||
|
const elapsed = Date.now() - startTime
|
||||||
|
const progress = elapsed / duration
|
||||||
|
|
||||||
|
if (progress >= 1) {
|
||||||
|
// 滚动结束,停在最终结果
|
||||||
|
if (scrollInterval) {
|
||||||
|
clearInterval(scrollInterval)
|
||||||
|
scrollInterval = null
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 速度逐渐减慢
|
||||||
|
const speed = progress < 0.7 ? 50 : 50 + (progress - 0.7) * 300
|
||||||
|
|
||||||
|
currentIndex = (currentIndex + 1) % availableIndices.length
|
||||||
|
highlightIndex.value = availableIndices[currentIndex]
|
||||||
|
}, 50)
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopScrollAnimation() {
|
||||||
|
if (scrollInterval) {
|
||||||
|
clearInterval(scrollInterval)
|
||||||
|
scrollInterval = null
|
||||||
|
}
|
||||||
|
|
||||||
|
// 找到最终中奖的卡片索引
|
||||||
|
if (props.lastResult) {
|
||||||
|
// 找到第一个未抽且匹配中奖奖品ID的卡片
|
||||||
|
const winnerIndex = prizeCards.value.findIndex(
|
||||||
|
card => !card.isDrawn && card.id === props.lastResult?.prizeId,
|
||||||
|
)
|
||||||
|
if (winnerIndex !== -1) {
|
||||||
|
highlightIndex.value = winnerIndex
|
||||||
|
winnerCardIndex.value = winnerIndex
|
||||||
|
|
||||||
|
// 标记这张卡片为已抽
|
||||||
|
if (initialCardOrder.value[winnerIndex]) {
|
||||||
|
initialCardOrder.value[winnerIndex].isDrawn = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function fireConfetti() {
|
||||||
|
if (!confettiCanvas.value)
|
||||||
|
return
|
||||||
|
|
||||||
|
const myConfetti = confetti.create(confettiCanvas.value, {
|
||||||
|
resize: true,
|
||||||
|
useWorker: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
const duration = 3000
|
||||||
|
const animationEnd = Date.now() + duration
|
||||||
|
|
||||||
|
const randomInRange = (min: number, max: number) => Math.random() * (max - min) + min
|
||||||
|
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
const timeLeft = animationEnd - Date.now()
|
||||||
|
|
||||||
|
if (timeLeft <= 0) {
|
||||||
|
clearInterval(interval)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const particleCount = 50 * (timeLeft / duration)
|
||||||
|
|
||||||
|
myConfetti({
|
||||||
|
particleCount,
|
||||||
|
startVelocity: 30,
|
||||||
|
spread: randomInRange(50, 70),
|
||||||
|
origin: {
|
||||||
|
x: randomInRange(0.1, 0.9),
|
||||||
|
y: Math.random() - 0.2,
|
||||||
|
},
|
||||||
|
colors: ['#FF6B6B', '#4ECDC4', '#45B7D1', '#FFA07A', '#98D8C8', '#F7DC6F'],
|
||||||
|
})
|
||||||
|
}, 250)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDraw() {
|
||||||
|
emit('draw')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.draw-area {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: 30px;
|
||||||
|
position: relative;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confetti-canvas {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.draw-initial {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.draw-icon {
|
||||||
|
font-size: 80px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
animation: float 3s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes float {
|
||||||
|
0%, 100% {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: translateY(-20px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.draw-title {
|
||||||
|
font-size: 32px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: white;
|
||||||
|
margin: 0 0 12px 0;
|
||||||
|
text-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.draw-subtitle {
|
||||||
|
font-size: 18px;
|
||||||
|
color: rgba(255, 255, 255, 0.9);
|
||||||
|
margin: 0;
|
||||||
|
text-shadow: 0 1px 5px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.prize-wall-container {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 1200px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prize-wall {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(8, 1fr);
|
||||||
|
gap: 12px;
|
||||||
|
padding: 20px;
|
||||||
|
|
||||||
|
@media (max-width: 1400px) {
|
||||||
|
grid-template-columns: repeat(6, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1000px) {
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.element-card {
|
||||||
|
aspect-ratio: 3 / 4;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 12px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
&.card-drawn {
|
||||||
|
opacity: 0.5;
|
||||||
|
filter: grayscale(0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.card-highlight {
|
||||||
|
transform: scale(1.1);
|
||||||
|
z-index: 10;
|
||||||
|
animation: pulse 0.5s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.card-winner {
|
||||||
|
transform: scale(1.15);
|
||||||
|
z-index: 20;
|
||||||
|
animation: winner-glow 1s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0%, 100% {
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: scale(1.15);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes winner-glow {
|
||||||
|
0%, 100% {
|
||||||
|
transform: scale(1.15);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: scale(1.2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-uid {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: rgba(255, 255, 255, 0.8);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-name {
|
||||||
|
font-size: 36px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: white;
|
||||||
|
text-align: center;
|
||||||
|
text-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-info {
|
||||||
|
font-size: 11px;
|
||||||
|
color: rgba(255, 255, 255, 0.7);
|
||||||
|
text-align: center;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-avatar {
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
border-radius: 50%;
|
||||||
|
object-fit: cover;
|
||||||
|
border: 2px solid rgba(255, 255, 255, 0.5);
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-mystery {
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 24px;
|
||||||
|
color: rgba(255, 255, 255, 0.5);
|
||||||
|
font-weight: bold;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.draw-actions {
|
||||||
|
margin-top: 20px;
|
||||||
|
position: relative;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-draw {
|
||||||
|
padding: 16px 48px;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 700;
|
||||||
|
border: none;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4);
|
||||||
|
|
||||||
|
&:hover:not(:disabled) {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.completed-message {
|
||||||
|
text-align: center;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.completed-icon {
|
||||||
|
font-size: 80px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
animation: celebrate 1s ease infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes celebrate {
|
||||||
|
0%, 100% {
|
||||||
|
transform: rotate(-10deg);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: rotate(10deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.completed-message h3 {
|
||||||
|
font-size: 32px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: white;
|
||||||
|
margin: 0 0 12px 0;
|
||||||
|
text-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.completed-message p {
|
||||||
|
font-size: 18px;
|
||||||
|
color: rgba(255, 255, 255, 0.9);
|
||||||
|
margin: 0;
|
||||||
|
text-shadow: 0 1px 5px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
237
src/views/PrizeDraw/components/DrawHistory.vue
Normal file
@@ -0,0 +1,237 @@
|
|||||||
|
<template>
|
||||||
|
<div class="draw-history">
|
||||||
|
<div v-if="results.length === 0" class="history-empty">
|
||||||
|
<div class="empty-icon">📝</div>
|
||||||
|
<p>暂无抽奖记录</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="history-list">
|
||||||
|
<div class="history-actions">
|
||||||
|
<button
|
||||||
|
class="btn-undo"
|
||||||
|
:disabled="results.length === 0"
|
||||||
|
@click="handleUndo"
|
||||||
|
>
|
||||||
|
↶ 撤销最后一次
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="history-scroll">
|
||||||
|
<div
|
||||||
|
v-for="result in results"
|
||||||
|
:key="result.id"
|
||||||
|
class="history-item"
|
||||||
|
>
|
||||||
|
<div class="history-badge">
|
||||||
|
<span class="history-index">#{{ result.drawIndex }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="history-content">
|
||||||
|
<div class="history-prize">
|
||||||
|
<span class="prize-name">{{ result.prizeName }}</span>
|
||||||
|
<span class="prize-desc">{{ result.prizeDescription }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="history-time">
|
||||||
|
{{ formatTime(result.drawTime) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { DrawResult } from '@/store/prizeDrawConfig'
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
results: DrawResult[]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
undo: []
|
||||||
|
}>()
|
||||||
|
|
||||||
|
function handleUndo() {
|
||||||
|
emit('undo')
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatTime(isoString: string): string {
|
||||||
|
const date = new Date(isoString)
|
||||||
|
const hours = String(date.getHours()).padStart(2, '0')
|
||||||
|
const minutes = String(date.getMinutes()).padStart(2, '0')
|
||||||
|
const seconds = String(date.getSeconds()).padStart(2, '0')
|
||||||
|
return `${hours}:${minutes}:${seconds}`
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.draw-history {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-empty {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: #95a5a6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-icon {
|
||||||
|
font-size: 48px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-empty p {
|
||||||
|
font-size: 14px;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-actions {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
padding-bottom: 12px;
|
||||||
|
border-bottom: 2px solid #ecf0f1;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-undo {
|
||||||
|
width: 100%;
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 13px;
|
||||||
|
border: none;
|
||||||
|
background: #f39c12;
|
||||||
|
color: white;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
|
||||||
|
&:hover:not(:disabled) {
|
||||||
|
background: #e67e22;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-scroll {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
padding-right: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-item {
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 12px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
animation: slideIn 0.3s ease;
|
||||||
|
border: 1px solid #ecf0f1;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #ecf0f1;
|
||||||
|
transform: translateX(-4px);
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
border: 2px solid #3498db;
|
||||||
|
box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-10px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-badge {
|
||||||
|
position: absolute;
|
||||||
|
top: -8px;
|
||||||
|
left: 12px;
|
||||||
|
background: #34495e;
|
||||||
|
color: white;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-index {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-content {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 8px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-person {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.person-id {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #2c3e50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.person-name {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #7f8c8d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-arrow {
|
||||||
|
font-size: 16px;
|
||||||
|
color: #3498db;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-prize {
|
||||||
|
flex: 1;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prize-name {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #e74c3c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-time {
|
||||||
|
font-size: 10px;
|
||||||
|
color: #95a5a6;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
183
src/views/PrizeDraw/components/PrizeCard.vue
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="prize-card"
|
||||||
|
:class="{ 'prize-card--empty': prize.remainingCount === 0 }"
|
||||||
|
>
|
||||||
|
<div v-if="showDetails" class="prize-content-detailed">
|
||||||
|
<div class="prize-icon">
|
||||||
|
{{ getPrizeIcon(prize.name) }}
|
||||||
|
</div>
|
||||||
|
<div class="prize-info">
|
||||||
|
<div class="prize-header">
|
||||||
|
<h3 class="prize-name">{{ prize.name }}</h3>
|
||||||
|
<div class="prize-count" :style="{ color: prize.remainingCount > 0 ? prize.color : '#95a5a6' }">
|
||||||
|
{{ prize.remainingCount }}/{{ prize.totalCount }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="prize-description">{{ prize.description }}</p>
|
||||||
|
<div class="prize-progress">
|
||||||
|
<div
|
||||||
|
class="prize-progress-bar"
|
||||||
|
:style="{
|
||||||
|
width: `${(prize.remainingCount / prize.totalCount) * 100}%`,
|
||||||
|
backgroundColor: prize.color
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else class="prize-content-simple">
|
||||||
|
<div class="mystery-box">
|
||||||
|
<div class="mystery-icon">?</div>
|
||||||
|
</div>
|
||||||
|
<div class="prize-count-simple" :style="{ color: prize.remainingCount > 0 ? prize.color : '#95a5a6' }">
|
||||||
|
{{ prize.remainingCount }}/{{ prize.totalCount }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="prize.remainingCount === 0" class="prize-empty-badge">
|
||||||
|
已抽完
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { PrizeConfig } from '@/store/prizeDrawConfig'
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
prize: PrizeConfig
|
||||||
|
showDetails: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
|
function getPrizeIcon(name: string): string {
|
||||||
|
const iconMap: Record<string, string> = {
|
||||||
|
'快乐通勤奖': '⏰',
|
||||||
|
'跑马场自由日': '🏠',
|
||||||
|
'前途光明奖': '💼',
|
||||||
|
'现金红包500元': '💰',
|
||||||
|
'现金红包300元': '💵',
|
||||||
|
'现金红包200元': '💴',
|
||||||
|
'现金红包100元': '💸',
|
||||||
|
}
|
||||||
|
return iconMap[name] || '🎁'
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.prize-card {
|
||||||
|
background: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 16px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
position: relative;
|
||||||
|
border: 2px solid #ecf0f1;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: translateX(4px);
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
&--empty {
|
||||||
|
opacity: 0.5;
|
||||||
|
background: #f8f9fa;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.prize-content-detailed {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prize-icon {
|
||||||
|
font-size: 32px;
|
||||||
|
line-height: 1;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prize-info {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prize-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prize-name {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 700;
|
||||||
|
margin: 0;
|
||||||
|
color: #2c3e50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prize-count {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 800;
|
||||||
|
min-width: 50px;
|
||||||
|
text-align: right;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prize-description {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #7f8c8d;
|
||||||
|
margin: 0 0 8px 0;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prize-progress {
|
||||||
|
height: 4px;
|
||||||
|
background: #ecf0f1;
|
||||||
|
border-radius: 2px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prize-progress-bar {
|
||||||
|
height: 100%;
|
||||||
|
transition: width 0.5s ease;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prize-content-simple {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mystery-box {
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
border-radius: 8px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mystery-icon {
|
||||||
|
font-size: 32px;
|
||||||
|
color: rgba(255, 255, 255, 0.5);
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prize-count-simple {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prize-empty-badge {
|
||||||
|
position: absolute;
|
||||||
|
top: 8px;
|
||||||
|
right: 8px;
|
||||||
|
background: #e74c3c;
|
||||||
|
color: white;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
949
src/views/PrizeDraw/index.vue
Normal file
@@ -0,0 +1,949 @@
|
|||||||
|
<template>
|
||||||
|
<div class="prize-draw-page">
|
||||||
|
<!-- 星空背景 -->
|
||||||
|
<StarsBackground :home-background="homeBackground" />
|
||||||
|
|
||||||
|
<!-- 顶部标题 -->
|
||||||
|
<div class="header-wrapper">
|
||||||
|
<div class="lingniu-title-row">
|
||||||
|
<span v-if="!isTextColor" class="title-ornament title-ornament--left" aria-hidden="true" />
|
||||||
|
<h2
|
||||||
|
class="page-title lingniu-title-text"
|
||||||
|
:class="{ 'lingniu-title-gradient': !isTextColor }"
|
||||||
|
:style="titleStyle"
|
||||||
|
>
|
||||||
|
{{ topTitle }}
|
||||||
|
</h2>
|
||||||
|
<span v-if="!isTextColor" class="title-ornament title-ornament--right" aria-hidden="true" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 抽奖进度和重置按钮 -->
|
||||||
|
<div class="prize-info">
|
||||||
|
<div class="progress-text" :style="{ color: textColor }">
|
||||||
|
已抽奖:{{ prizeDrawStore.currentDrawIndex }} / 88
|
||||||
|
</div>
|
||||||
|
<button class="reset-button" @click="handleReset">
|
||||||
|
重置抽奖
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 3D容器 -->
|
||||||
|
<div id="prize-container" ref="containerRef" class="container-3d" />
|
||||||
|
|
||||||
|
<!-- 操作按钮 -->
|
||||||
|
<OptionButton
|
||||||
|
:current-status="currentStatus"
|
||||||
|
:table-data="tableData"
|
||||||
|
:enter-lottery="enterLottery"
|
||||||
|
:start-lottery="startLottery"
|
||||||
|
:stop-lottery="stopLottery"
|
||||||
|
:continue-lottery="continueLottery"
|
||||||
|
:quit-lottery="quitLottery"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { nextTick, onMounted, onUnmounted, ref, computed } from 'vue'
|
||||||
|
import { storeToRefs } from 'pinia'
|
||||||
|
import { PerspectiveCamera, Scene } from 'three'
|
||||||
|
import { CSS3DObject, CSS3DRenderer } from 'three-css3d'
|
||||||
|
import { TrackballControls } from 'three/examples/jsm/controls/TrackballControls.js'
|
||||||
|
import * as TWEEN from '@tweenjs/tween.js'
|
||||||
|
import useStore from '@/store'
|
||||||
|
import { usePrizeDrawStore } from '@/store/prizeDrawConfig'
|
||||||
|
import type { DrawResult } from '@/store/prizeDrawConfig'
|
||||||
|
import StarsBackground from '@/views/Home/components/StarsBackground/index.vue'
|
||||||
|
import OptionButton from '@/views/Home/components/OptionsButton/index.vue'
|
||||||
|
import { useElementStyle, useElementPosition } from '@/hooks/useElement'
|
||||||
|
import { createTableVertices, createSphereVertices, confettiFire, initTableData, getRandomElements } from '@/views/Home/utils'
|
||||||
|
import { rgba, rgbToHex } from '@/utils/color'
|
||||||
|
import { selectCard } from '@/utils'
|
||||||
|
import { LotteryStatus } from '@/views/Home/type'
|
||||||
|
import type { IPersonConfig } from '@/types/storeType'
|
||||||
|
|
||||||
|
// Store
|
||||||
|
const { personConfig, globalConfig, prizeConfig } = useStore()
|
||||||
|
const prizeDrawStore = usePrizeDrawStore()
|
||||||
|
const {
|
||||||
|
getAllPersonList: allPersonList,
|
||||||
|
getNotPersonList: notPersonList,
|
||||||
|
getNotThisPrizePersonList: notThisPrizePersonList,
|
||||||
|
} = storeToRefs(personConfig)
|
||||||
|
const { getCurrentPrize: currentPrize } = storeToRefs(prizeConfig)
|
||||||
|
const {
|
||||||
|
getBackground: homeBackground,
|
||||||
|
getCardColor: cardColor,
|
||||||
|
getPatterColor: patternColor,
|
||||||
|
getPatternList: patternList,
|
||||||
|
getTextColor: textColor,
|
||||||
|
getLuckyColor: luckyColor,
|
||||||
|
getCardSize: cardSize,
|
||||||
|
getTextSize: textSize,
|
||||||
|
getRowCount: rowCount,
|
||||||
|
getTitleFont: titleFont,
|
||||||
|
getTitleFontSyncGlobal: titleFontSyncGlobal,
|
||||||
|
getTopTitle: topTitle,
|
||||||
|
} = storeToRefs(globalConfig)
|
||||||
|
|
||||||
|
// 标题样式计算
|
||||||
|
const isTextColor = computed(() => {
|
||||||
|
return rgbToHex(textColor.value) !== '#00000000'
|
||||||
|
})
|
||||||
|
|
||||||
|
const titleStyle = computed(() => {
|
||||||
|
const style: any = {
|
||||||
|
fontSize: `${textSize.value * 1.5}px`,
|
||||||
|
}
|
||||||
|
if (!titleFontSyncGlobal.value) {
|
||||||
|
style.fontFamily = titleFont.value
|
||||||
|
}
|
||||||
|
if (isTextColor.value) {
|
||||||
|
style.color = textColor.value
|
||||||
|
}
|
||||||
|
return style
|
||||||
|
})
|
||||||
|
|
||||||
|
// Three.js 相关
|
||||||
|
const containerRef = ref<HTMLElement>()
|
||||||
|
const scene = ref<Scene>()
|
||||||
|
const camera = ref<PerspectiveCamera>()
|
||||||
|
const renderer = ref<CSS3DRenderer>()
|
||||||
|
const controls = ref<TrackballControls>()
|
||||||
|
const objects = ref<any[]>([])
|
||||||
|
const targets: any = {
|
||||||
|
table: [],
|
||||||
|
sphere: [],
|
||||||
|
}
|
||||||
|
const animationFrameId = ref<number>()
|
||||||
|
|
||||||
|
// 页面状态
|
||||||
|
const currentStatus = ref<LotteryStatus>(LotteryStatus.init)
|
||||||
|
const canOperate = ref(true)
|
||||||
|
const isInitialDone = ref(false)
|
||||||
|
const tableData = ref<IPersonConfig[]>([])
|
||||||
|
const luckyTargets = ref<IPersonConfig[]>([])
|
||||||
|
const luckyCardList = ref<number[]>([])
|
||||||
|
const luckyCount = ref(1) // 每次抽1个
|
||||||
|
const personPool = ref<IPersonConfig[]>([])
|
||||||
|
const intervalTimer = ref<any>(null)
|
||||||
|
const currentDrawResult = ref<DrawResult | null>(null) // 保存当前抽奖结果
|
||||||
|
|
||||||
|
// 初始化 Three.js
|
||||||
|
function initThreeJs() {
|
||||||
|
console.log('初始化 Three.js 场景...')
|
||||||
|
|
||||||
|
const width = window.innerWidth
|
||||||
|
const height = window.innerHeight
|
||||||
|
|
||||||
|
scene.value = new Scene()
|
||||||
|
camera.value = new PerspectiveCamera(40, width / height, 1, 10000)
|
||||||
|
camera.value.position.z = 3000
|
||||||
|
|
||||||
|
renderer.value = new CSS3DRenderer()
|
||||||
|
renderer.value.setSize(width, height * 0.9)
|
||||||
|
renderer.value.domElement.style.position = 'absolute'
|
||||||
|
renderer.value.domElement.style.paddingTop = '50px'
|
||||||
|
renderer.value.domElement.style.top = '50%'
|
||||||
|
renderer.value.domElement.style.left = '50%'
|
||||||
|
renderer.value.domElement.style.transform = 'translate(-50%, -50%)'
|
||||||
|
containerRef.value!.appendChild(renderer.value.domElement)
|
||||||
|
|
||||||
|
controls.value = new TrackballControls(camera.value, renderer.value.domElement)
|
||||||
|
controls.value.minDistance = 500
|
||||||
|
controls.value.maxDistance = 6000
|
||||||
|
controls.value.addEventListener('change', render)
|
||||||
|
|
||||||
|
console.log('创建卡片...')
|
||||||
|
|
||||||
|
// 创建卡片
|
||||||
|
for (let i = 0; i < tableData.value.length; i++) {
|
||||||
|
const element = document.createElement('div')
|
||||||
|
element.className = 'element-card'
|
||||||
|
|
||||||
|
const number = document.createElement('div')
|
||||||
|
number.className = 'card-id'
|
||||||
|
number.textContent = tableData.value[i].uid
|
||||||
|
element.appendChild(number)
|
||||||
|
|
||||||
|
const symbol = document.createElement('div')
|
||||||
|
symbol.className = 'card-name'
|
||||||
|
symbol.textContent = tableData.value[i].name
|
||||||
|
element.appendChild(symbol)
|
||||||
|
|
||||||
|
const details = document.createElement('div')
|
||||||
|
details.className = 'card-detail'
|
||||||
|
details.innerHTML = `${tableData.value[i].department}<br/>${tableData.value[i].identity}`
|
||||||
|
element.appendChild(details)
|
||||||
|
|
||||||
|
// 第4个子元素:avatar(空div,因为不显示头像)
|
||||||
|
const avatarEmpty = document.createElement('div')
|
||||||
|
avatarEmpty.style.display = 'none'
|
||||||
|
element.appendChild(avatarEmpty)
|
||||||
|
|
||||||
|
const styledElement = useElementStyle({
|
||||||
|
element,
|
||||||
|
person: tableData.value[i],
|
||||||
|
index: i,
|
||||||
|
patternList: patternList.value,
|
||||||
|
patternColor: patternColor.value,
|
||||||
|
cardColor: cardColor.value,
|
||||||
|
cardSize: { width: cardSize.value.width, height: cardSize.value.height },
|
||||||
|
scale: 1,
|
||||||
|
textSize: textSize.value,
|
||||||
|
mod: 'default',
|
||||||
|
})
|
||||||
|
|
||||||
|
const objectCSS = new CSS3DObject(styledElement)
|
||||||
|
objectCSS.position.x = Math.random() * 4000 - 2000
|
||||||
|
objectCSS.position.y = Math.random() * 4000 - 2000
|
||||||
|
objectCSS.position.z = Math.random() * 4000 - 2000
|
||||||
|
|
||||||
|
scene.value.add(objectCSS)
|
||||||
|
objects.value.push(objectCSS)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建目标位置
|
||||||
|
targets.table = createTableVertices({
|
||||||
|
tableData: tableData.value,
|
||||||
|
rowCount: rowCount.value,
|
||||||
|
cardSize: { width: cardSize.value.width, height: cardSize.value.height },
|
||||||
|
})
|
||||||
|
|
||||||
|
targets.sphere = createSphereVertices({ objectsLength: objects.value.length })
|
||||||
|
|
||||||
|
window.addEventListener('resize', onWindowResize, false)
|
||||||
|
|
||||||
|
// 初始聚合到表格
|
||||||
|
transform(targets.table, 1000)
|
||||||
|
|
||||||
|
// 开始动画循环
|
||||||
|
animation()
|
||||||
|
|
||||||
|
isInitialDone.value = true
|
||||||
|
console.log('Three.js 初始化完成')
|
||||||
|
}
|
||||||
|
|
||||||
|
function render() {
|
||||||
|
if (renderer.value && scene.value && camera.value) {
|
||||||
|
renderer.value.render(scene.value, camera.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function animation() {
|
||||||
|
animationFrameId.value = requestAnimationFrame(animation)
|
||||||
|
TWEEN.update()
|
||||||
|
controls.value?.update()
|
||||||
|
}
|
||||||
|
|
||||||
|
function transform(targetPositions: any[], duration: number) {
|
||||||
|
TWEEN.removeAll()
|
||||||
|
|
||||||
|
for (let i = 0; i < objects.value.length; i++) {
|
||||||
|
const object = objects.value[i]
|
||||||
|
const target = targetPositions[i]
|
||||||
|
|
||||||
|
new TWEEN.Tween(object.position)
|
||||||
|
.to({
|
||||||
|
x: target.position.x,
|
||||||
|
y: target.position.y,
|
||||||
|
z: target.position.z,
|
||||||
|
}, Math.random() * duration + duration)
|
||||||
|
.easing(TWEEN.Easing.Exponential.InOut)
|
||||||
|
.start()
|
||||||
|
|
||||||
|
new TWEEN.Tween(object.rotation)
|
||||||
|
.to({
|
||||||
|
x: target.rotation.x,
|
||||||
|
y: target.rotation.y,
|
||||||
|
z: target.rotation.z,
|
||||||
|
}, Math.random() * duration + duration)
|
||||||
|
.easing(TWEEN.Easing.Exponential.InOut)
|
||||||
|
.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
new TWEEN.Tween({})
|
||||||
|
.to({}, duration * 2)
|
||||||
|
.onUpdate(render)
|
||||||
|
.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
function rollBall(rotateY: number, duration: number) {
|
||||||
|
TWEEN.removeAll()
|
||||||
|
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
if (!scene.value) {
|
||||||
|
resolve('')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
scene.value.rotation.y = 0
|
||||||
|
const ballRotationY = Math.PI * rotateY * 1000
|
||||||
|
|
||||||
|
new TWEEN.Tween(scene.value.rotation)
|
||||||
|
.to({ x: 0, y: ballRotationY, z: 0 }, duration * 1000)
|
||||||
|
.onUpdate(render)
|
||||||
|
.start()
|
||||||
|
.onStop(() => resolve(''))
|
||||||
|
.onComplete(() => resolve(''))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function onWindowResize() {
|
||||||
|
if (!camera.value || !renderer.value)
|
||||||
|
return
|
||||||
|
|
||||||
|
camera.value.aspect = window.innerWidth / window.innerHeight
|
||||||
|
camera.value.updateProjectionMatrix()
|
||||||
|
renderer.value.setSize(window.innerWidth, window.innerHeight)
|
||||||
|
render()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 抽奖流程
|
||||||
|
async function enterLottery() {
|
||||||
|
if (!canOperate.value)
|
||||||
|
return
|
||||||
|
|
||||||
|
canOperate.value = false
|
||||||
|
await transform(targets.sphere, 1000)
|
||||||
|
currentStatus.value = LotteryStatus.ready
|
||||||
|
canOperate.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function startLottery() {
|
||||||
|
if (!canOperate.value)
|
||||||
|
return
|
||||||
|
|
||||||
|
console.log('开始抽奖')
|
||||||
|
|
||||||
|
// 检查是否还有奖品
|
||||||
|
if (prizeDrawStore.prizePool.length === 0) {
|
||||||
|
console.log('所有奖品已抽完')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用 prizeDrawStore 执行抽奖
|
||||||
|
prizeDrawStore.executeDraw().then((result: DrawResult | null) => {
|
||||||
|
if (!result) {
|
||||||
|
console.log('抽奖失败或已完成')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('抽奖结果:', result)
|
||||||
|
|
||||||
|
// 保存抽奖结果
|
||||||
|
currentDrawResult.value = result
|
||||||
|
|
||||||
|
// 随机选择一张卡片来展示中奖奖品
|
||||||
|
const randomPerson = tableData.value[Math.floor(Math.random() * tableData.value.length)]
|
||||||
|
luckyTargets.value = [randomPerson]
|
||||||
|
|
||||||
|
console.log('展示卡片:', randomPerson.name, '中奖:', result.prizeName)
|
||||||
|
|
||||||
|
currentStatus.value = LotteryStatus.running
|
||||||
|
rollBall(10, 3000)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function stopLottery() {
|
||||||
|
if (!canOperate.value)
|
||||||
|
return
|
||||||
|
|
||||||
|
console.log('停止抽奖,展示结果')
|
||||||
|
console.log('中奖结果:', currentDrawResult.value)
|
||||||
|
|
||||||
|
if (!currentDrawResult.value) {
|
||||||
|
console.error('没有抽奖结果')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
canOperate.value = false
|
||||||
|
|
||||||
|
// 停止旋转并等待完成
|
||||||
|
TWEEN.removeAll()
|
||||||
|
|
||||||
|
// 立即将场景旋转归零
|
||||||
|
if (scene.value) {
|
||||||
|
scene.value.rotation.y = 0
|
||||||
|
scene.value.rotation.x = 0
|
||||||
|
scene.value.rotation.z = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置相机和控制器到初始位置
|
||||||
|
if (camera.value && controls.value) {
|
||||||
|
camera.value.position.set(0, 0, 3000)
|
||||||
|
camera.value.lookAt(0, 0, 0)
|
||||||
|
controls.value.target.set(0, 0, 0)
|
||||||
|
controls.value.update()
|
||||||
|
}
|
||||||
|
|
||||||
|
render()
|
||||||
|
|
||||||
|
// 等待一小段时间确保场景稳定
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 100))
|
||||||
|
|
||||||
|
const windowSize = { width: window.innerWidth, height: window.innerHeight }
|
||||||
|
|
||||||
|
luckyTargets.value.forEach((person: IPersonConfig, index: number) => {
|
||||||
|
const cardIndex = selectCard(luckyCardList.value, tableData.value.length, person.id)
|
||||||
|
luckyCardList.value.push(cardIndex)
|
||||||
|
|
||||||
|
console.log('中奖卡片索引:', cardIndex)
|
||||||
|
|
||||||
|
const item = objects.value[cardIndex]
|
||||||
|
|
||||||
|
// 使用更大的 scale 来显示中奖卡片
|
||||||
|
const luckyScale = 2.5
|
||||||
|
const { xTable, yTable } = useElementPosition(
|
||||||
|
item,
|
||||||
|
rowCount.value,
|
||||||
|
luckyTargets.value.length,
|
||||||
|
{ width: cardSize.value.width, height: cardSize.value.height },
|
||||||
|
windowSize,
|
||||||
|
index,
|
||||||
|
)
|
||||||
|
|
||||||
|
console.log('中奖卡片位置:', { xTable, yTable, scale: luckyScale })
|
||||||
|
|
||||||
|
new TWEEN.Tween(item.position)
|
||||||
|
.to({ x: xTable, y: yTable, z: 1000 }, 1200)
|
||||||
|
.easing(TWEEN.Easing.Exponential.InOut)
|
||||||
|
.onStart(() => {
|
||||||
|
console.log('开始移动中奖卡片,更新为奖品信息')
|
||||||
|
|
||||||
|
// CSS3DObject 的 element 属性
|
||||||
|
const element = (item as any).element
|
||||||
|
|
||||||
|
if (!element) {
|
||||||
|
console.error('element 不存在!')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新卡片内容为奖品信息
|
||||||
|
const prizeResult = currentDrawResult.value!
|
||||||
|
|
||||||
|
console.log('更新卡片内容:', prizeResult)
|
||||||
|
console.log('element 子元素数量:', element.children.length)
|
||||||
|
|
||||||
|
// 更新卡片文本
|
||||||
|
element.children[0].textContent = '' // 不显示序号
|
||||||
|
element.children[1].textContent = prizeResult.prizeName // 奖品名称
|
||||||
|
element.children[2].innerHTML = prizeResult.prizeDescription // 奖品描述
|
||||||
|
|
||||||
|
// 更新样式 - 使用更大的尺寸
|
||||||
|
element.style.backgroundColor = rgba(luckyColor.value, 0.9)
|
||||||
|
element.style.border = `2px solid ${rgba(luckyColor.value, 0.85)}`
|
||||||
|
element.style.boxShadow = `0 0 20px ${rgba(luckyColor.value, 0.8)}`
|
||||||
|
element.style.width = `${cardSize.value.width * luckyScale}px`
|
||||||
|
element.style.height = `${cardSize.value.height * luckyScale}px`
|
||||||
|
element.style.padding = '20px'
|
||||||
|
element.style.display = 'flex'
|
||||||
|
element.style.flexDirection = 'column'
|
||||||
|
element.style.justifyContent = 'center'
|
||||||
|
element.style.alignItems = 'center'
|
||||||
|
element.style.overflow = 'hidden'
|
||||||
|
element.className = 'lucky-element-card'
|
||||||
|
|
||||||
|
// 隐藏序号
|
||||||
|
element.children[0].style.display = 'none'
|
||||||
|
|
||||||
|
// 判断是否是红包奖项,调整描述文字大小
|
||||||
|
const isRedPacket = prizeResult.prizeName.includes('红包')
|
||||||
|
const descFontScale = isRedPacket ? 1.0 : 0.5
|
||||||
|
|
||||||
|
element.children[1].style.fontSize = `${textSize.value * luckyScale * 1.0}px`
|
||||||
|
element.children[1].style.lineHeight = '1.4'
|
||||||
|
element.children[1].style.fontWeight = 'bold'
|
||||||
|
element.children[1].style.color = '#fff'
|
||||||
|
element.children[1].style.textShadow = `0 0 15px ${rgba(luckyColor.value, 0.95)}`
|
||||||
|
element.children[1].style.marginBottom = '15px'
|
||||||
|
element.children[1].style.textAlign = 'center'
|
||||||
|
element.children[1].style.wordWrap = 'break-word'
|
||||||
|
element.children[1].style.maxWidth = '100%'
|
||||||
|
element.children[1].style.whiteSpace = 'normal'
|
||||||
|
element.children[1].style.overflow = 'visible'
|
||||||
|
element.children[1].style.opacity = '1' // 显示文字
|
||||||
|
|
||||||
|
element.children[2].style.fontSize = `${textSize.value * luckyScale * descFontScale}px`
|
||||||
|
element.children[2].style.color = '#fff'
|
||||||
|
element.children[2].style.lineHeight = '1.5'
|
||||||
|
element.children[2].style.textAlign = 'center'
|
||||||
|
element.children[2].style.wordWrap = 'break-word'
|
||||||
|
element.children[2].style.maxWidth = '100%'
|
||||||
|
element.children[2].style.overflow = 'hidden'
|
||||||
|
element.children[2].style.display = '-webkit-box'
|
||||||
|
element.children[2].style.webkitLineClamp = '3'
|
||||||
|
element.children[2].style.webkitBoxOrient = 'vertical'
|
||||||
|
element.children[2].style.opacity = '1' // 显示文字
|
||||||
|
|
||||||
|
console.log('卡片内容已更新:', prizeResult.prizeName)
|
||||||
|
})
|
||||||
|
.onUpdate(render)
|
||||||
|
.start()
|
||||||
|
.onComplete(() => {
|
||||||
|
console.log('中奖卡片移动完成')
|
||||||
|
canOperate.value = true
|
||||||
|
currentStatus.value = LotteryStatus.end
|
||||||
|
})
|
||||||
|
|
||||||
|
new TWEEN.Tween(item.rotation)
|
||||||
|
.to({ x: 0, y: 0, z: 0 }, 900)
|
||||||
|
.easing(TWEEN.Easing.Exponential.InOut)
|
||||||
|
.onUpdate(render)
|
||||||
|
.start()
|
||||||
|
.onComplete(() => {
|
||||||
|
console.log('开始礼花')
|
||||||
|
confettiFire(0, 5)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function continueLottery() {
|
||||||
|
// 检查是否还有奖品可抽
|
||||||
|
if (prizeDrawStore.prizePool.length === 0) {
|
||||||
|
console.log('所有奖品已抽完')
|
||||||
|
currentStatus.value = LotteryStatus.init
|
||||||
|
await transform(targets.table, 1000)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('继续抽奖,重置卡片')
|
||||||
|
|
||||||
|
// 恢复中奖卡片的原始样式
|
||||||
|
luckyCardList.value.forEach((cardIndex) => {
|
||||||
|
const item = objects.value[cardIndex]
|
||||||
|
const element = (item as any).element
|
||||||
|
|
||||||
|
if (element) {
|
||||||
|
const person = tableData.value[cardIndex]
|
||||||
|
|
||||||
|
// 恢复原始内容
|
||||||
|
element.children[0].textContent = person.uid
|
||||||
|
element.children[1].textContent = person.name
|
||||||
|
element.children[2].innerHTML = `${person.department}<br/>${person.identity}`
|
||||||
|
|
||||||
|
// 恢复原始样式
|
||||||
|
element.style.backgroundColor = rgba(cardColor.value, 0.8)
|
||||||
|
element.style.border = `1px solid ${rgba(cardColor.value, 0.75)}`
|
||||||
|
element.style.boxShadow = ''
|
||||||
|
element.style.width = `${cardSize.value.width}px`
|
||||||
|
element.style.height = `${cardSize.value.height}px`
|
||||||
|
element.style.padding = ''
|
||||||
|
element.style.display = ''
|
||||||
|
element.style.flexDirection = ''
|
||||||
|
element.style.justifyContent = ''
|
||||||
|
element.style.alignItems = ''
|
||||||
|
element.style.overflow = ''
|
||||||
|
element.className = 'element-card'
|
||||||
|
|
||||||
|
// 恢复序号显示
|
||||||
|
element.children[0].style.display = ''
|
||||||
|
|
||||||
|
// 恢复文字样式
|
||||||
|
element.children[0].style.fontSize = `${textSize.value * 0.5}px`
|
||||||
|
element.children[0].style.fontWeight = ''
|
||||||
|
element.children[0].style.color = textColor.value
|
||||||
|
element.children[0].style.marginBottom = ''
|
||||||
|
element.children[0].style.whiteSpace = ''
|
||||||
|
element.children[0].style.overflow = ''
|
||||||
|
element.children[0].style.textOverflow = ''
|
||||||
|
|
||||||
|
element.children[1].style.fontSize = `${textSize.value}px`
|
||||||
|
element.children[1].style.lineHeight = `${textSize.value * 3}px`
|
||||||
|
element.children[1].style.fontWeight = ''
|
||||||
|
element.children[1].style.color = textColor.value
|
||||||
|
element.children[1].style.textShadow = ''
|
||||||
|
element.children[1].style.marginBottom = ''
|
||||||
|
element.children[1].style.textAlign = ''
|
||||||
|
element.children[1].style.wordWrap = ''
|
||||||
|
element.children[1].style.maxWidth = ''
|
||||||
|
element.children[1].style.whiteSpace = ''
|
||||||
|
element.children[1].style.overflow = ''
|
||||||
|
|
||||||
|
element.children[2].style.fontSize = `${textSize.value * 0.5}px`
|
||||||
|
element.children[2].style.color = textColor.value
|
||||||
|
element.children[2].style.lineHeight = ''
|
||||||
|
element.children[2].style.textAlign = ''
|
||||||
|
element.children[2].style.wordWrap = ''
|
||||||
|
element.children[2].style.maxWidth = ''
|
||||||
|
element.children[2].style.overflow = ''
|
||||||
|
element.children[2].style.display = ''
|
||||||
|
element.children[2].style.webkitLineClamp = ''
|
||||||
|
element.children[2].style.webkitBoxOrient = ''
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 重置状态
|
||||||
|
luckyTargets.value = []
|
||||||
|
luckyCardList.value = []
|
||||||
|
currentDrawResult.value = null
|
||||||
|
|
||||||
|
// 先将所有卡片移回表格位置,确保整齐
|
||||||
|
await transform(targets.table, 800)
|
||||||
|
|
||||||
|
// 等待一小段时间
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 200))
|
||||||
|
|
||||||
|
// 再将所有卡片移到球体
|
||||||
|
await transform(targets.sphere, 1000)
|
||||||
|
|
||||||
|
// 等待球体动画完全完成
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1200))
|
||||||
|
|
||||||
|
currentStatus.value = LotteryStatus.ready
|
||||||
|
|
||||||
|
// 自动开始下一轮抽奖
|
||||||
|
setTimeout(() => {
|
||||||
|
startLottery()
|
||||||
|
}, 300)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function quitLottery() {
|
||||||
|
console.log('取消抽奖,恢复卡片')
|
||||||
|
|
||||||
|
// 恢复中奖卡片的原始样式
|
||||||
|
luckyCardList.value.forEach((cardIndex) => {
|
||||||
|
const item = objects.value[cardIndex]
|
||||||
|
const element = (item as any).element
|
||||||
|
|
||||||
|
if (element) {
|
||||||
|
const person = tableData.value[cardIndex]
|
||||||
|
|
||||||
|
// 恢复原始内容
|
||||||
|
element.children[0].textContent = person.uid
|
||||||
|
element.children[1].textContent = person.name
|
||||||
|
element.children[2].innerHTML = `${person.department}<br/>${person.identity}`
|
||||||
|
|
||||||
|
// 恢复原始样式
|
||||||
|
element.style.backgroundColor = rgba(cardColor.value, 0.8)
|
||||||
|
element.style.border = `1px solid ${rgba(cardColor.value, 0.75)}`
|
||||||
|
element.style.boxShadow = ''
|
||||||
|
element.style.width = `${cardSize.value.width}px`
|
||||||
|
element.style.height = `${cardSize.value.height}px`
|
||||||
|
element.style.padding = ''
|
||||||
|
element.style.display = ''
|
||||||
|
element.style.flexDirection = ''
|
||||||
|
element.style.justifyContent = ''
|
||||||
|
element.style.alignItems = ''
|
||||||
|
element.style.overflow = ''
|
||||||
|
element.className = 'element-card'
|
||||||
|
|
||||||
|
// 恢复序号显示
|
||||||
|
element.children[0].style.display = ''
|
||||||
|
|
||||||
|
// 恢复文字样式并隐藏文字
|
||||||
|
element.children[0].style.fontSize = `${textSize.value * 0.5}px`
|
||||||
|
element.children[0].style.fontWeight = ''
|
||||||
|
element.children[0].style.color = textColor.value
|
||||||
|
element.children[0].style.marginBottom = ''
|
||||||
|
element.children[0].style.whiteSpace = ''
|
||||||
|
element.children[0].style.overflow = ''
|
||||||
|
element.children[0].style.textOverflow = ''
|
||||||
|
element.children[0].style.opacity = '0' // 隐藏文字
|
||||||
|
|
||||||
|
element.children[1].style.fontSize = `${textSize.value}px`
|
||||||
|
element.children[1].style.lineHeight = `${textSize.value * 3}px`
|
||||||
|
element.children[1].style.fontWeight = ''
|
||||||
|
element.children[1].style.color = textColor.value
|
||||||
|
element.children[1].style.textShadow = ''
|
||||||
|
element.children[1].style.marginBottom = ''
|
||||||
|
element.children[1].style.textAlign = ''
|
||||||
|
element.children[1].style.wordWrap = ''
|
||||||
|
element.children[1].style.maxWidth = ''
|
||||||
|
element.children[1].style.whiteSpace = ''
|
||||||
|
element.children[1].style.overflow = ''
|
||||||
|
element.children[1].style.opacity = '0' // 隐藏文字
|
||||||
|
|
||||||
|
element.children[2].style.fontSize = `${textSize.value * 0.5}px`
|
||||||
|
element.children[2].style.color = textColor.value
|
||||||
|
element.children[2].style.lineHeight = ''
|
||||||
|
element.children[2].style.textAlign = ''
|
||||||
|
element.children[2].style.wordWrap = ''
|
||||||
|
element.children[2].style.maxWidth = ''
|
||||||
|
element.children[2].style.overflow = ''
|
||||||
|
element.children[2].style.display = ''
|
||||||
|
element.children[2].style.webkitLineClamp = ''
|
||||||
|
element.children[2].style.webkitBoxOrient = ''
|
||||||
|
element.children[2].style.opacity = '0' // 隐藏文字
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 重置状态
|
||||||
|
luckyTargets.value = []
|
||||||
|
luckyCardList.value = []
|
||||||
|
currentDrawResult.value = null
|
||||||
|
|
||||||
|
await transform(targets.table, 1000)
|
||||||
|
currentStatus.value = LotteryStatus.init
|
||||||
|
}
|
||||||
|
|
||||||
|
function setDefaultPersonList() {
|
||||||
|
personConfig.setDefaultPersonList()
|
||||||
|
window.location.reload()
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleReset() {
|
||||||
|
// 弹出确认对话框
|
||||||
|
const confirmed = window.confirm('确定要重置抽奖吗?这将清除所有抽奖记录并重新开始。')
|
||||||
|
|
||||||
|
if (confirmed) {
|
||||||
|
console.log('重置抽奖系统')
|
||||||
|
|
||||||
|
// 重置 prizeDrawStore
|
||||||
|
prizeDrawStore.reset()
|
||||||
|
|
||||||
|
// 重新加载页面
|
||||||
|
window.location.reload()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function cleanup() {
|
||||||
|
TWEEN.removeAll()
|
||||||
|
|
||||||
|
if (animationFrameId.value) {
|
||||||
|
cancelAnimationFrame(animationFrameId.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (intervalTimer.value) {
|
||||||
|
clearInterval(intervalTimer.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (scene.value) {
|
||||||
|
scene.value.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (controls.value) {
|
||||||
|
controls.value.removeEventListener('change', render)
|
||||||
|
controls.value.dispose()
|
||||||
|
}
|
||||||
|
|
||||||
|
window.removeEventListener('resize', onWindowResize)
|
||||||
|
}
|
||||||
|
|
||||||
|
const init = () => {
|
||||||
|
console.log('初始化页面')
|
||||||
|
|
||||||
|
// 初始化 prizeDrawStore
|
||||||
|
if (!prizeDrawStore.isInitialized) {
|
||||||
|
prizeDrawStore.init()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用 initTableData 来正确设置 x, y 坐标
|
||||||
|
tableData.value = initTableData({
|
||||||
|
allPersonList: prizeDrawStore.personPool,
|
||||||
|
rowCount: rowCount.value,
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log('卡片数据:', tableData.value.length)
|
||||||
|
console.log('第一张卡片坐标:', tableData.value[0]?.x, tableData.value[0]?.y)
|
||||||
|
|
||||||
|
initThreeJs()
|
||||||
|
containerRef.value!.style.color = textColor.value
|
||||||
|
isInitialDone.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
init()
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
nextTick(() => {
|
||||||
|
cleanup()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.prize-draw-page {
|
||||||
|
width: 100%;
|
||||||
|
height: 100vh;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container-3d {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-wrapper {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
z-index: 100;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lingniu-title-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
white-space: nowrap;
|
||||||
|
gap: 28px;
|
||||||
|
padding-top: 48px;
|
||||||
|
margin-bottom: 48px;
|
||||||
|
-webkit-animation: tracking-in-expand-fwd 0.8s cubic-bezier(0.215, 0.610, 0.355, 1.000) both;
|
||||||
|
animation: tracking-in-expand-fwd 0.8s cubic-bezier(0.215, 0.610, 0.355, 1.000) both;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-title {
|
||||||
|
margin: 0;
|
||||||
|
text-align: center;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lingniu-title-text {
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.22em;
|
||||||
|
padding-inline: 0.22em;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lingniu-title-gradient {
|
||||||
|
background: linear-gradient(
|
||||||
|
110deg,
|
||||||
|
#2F2828 0%,
|
||||||
|
#007143 18%,
|
||||||
|
#12a86b 36%,
|
||||||
|
#f5d27a 50%,
|
||||||
|
#12a86b 64%,
|
||||||
|
#007143 82%,
|
||||||
|
#2F2828 100%
|
||||||
|
);
|
||||||
|
background-size: 220% 100%;
|
||||||
|
background-clip: text;
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
color: transparent;
|
||||||
|
filter: drop-shadow(0 6px 24px rgba(0, 113, 67, 0.35));
|
||||||
|
animation: lingniu-shine 6s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-ornament {
|
||||||
|
--ornament-w: clamp(56px, 12vw, 140px);
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
width: var(--ornament-w);
|
||||||
|
height: 1px;
|
||||||
|
flex: none;
|
||||||
|
background: linear-gradient(
|
||||||
|
to right,
|
||||||
|
transparent 0%,
|
||||||
|
rgba(0, 113, 67, 0.05) 10%,
|
||||||
|
#007143 50%,
|
||||||
|
rgba(0, 113, 67, 0.05) 90%,
|
||||||
|
transparent 100%
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-ornament::before,
|
||||||
|
.title-ornament::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
transform: translateY(-50%) rotate(45deg);
|
||||||
|
background: linear-gradient(135deg, #f5d27a 0%, #d4a960 100%);
|
||||||
|
box-shadow:
|
||||||
|
0 0 14px rgba(0, 113, 67, 0.55),
|
||||||
|
0 0 4px rgba(245, 210, 122, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-ornament--left::before {
|
||||||
|
left: 50%;
|
||||||
|
margin-left: -4px;
|
||||||
|
background: linear-gradient(135deg, #007143 0%, #12a86b 100%);
|
||||||
|
}
|
||||||
|
.title-ornament--left::after {
|
||||||
|
right: -4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-ornament--right::before {
|
||||||
|
left: -4px;
|
||||||
|
}
|
||||||
|
.title-ornament--right::after {
|
||||||
|
right: 50%;
|
||||||
|
margin-right: -4px;
|
||||||
|
background: linear-gradient(135deg, #007143 0%, #12a86b 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.lingniu-title-row {
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
.title-ornament {
|
||||||
|
--ornament-w: 36px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes lingniu-shine {
|
||||||
|
0% { background-position: 200% 50%; }
|
||||||
|
100% { background-position: -200% 50%; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes tracking-in-expand-fwd {
|
||||||
|
0% {
|
||||||
|
letter-spacing: -0.5em;
|
||||||
|
transform: translateZ(-700px);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
40% { opacity: 0.6; }
|
||||||
|
100% {
|
||||||
|
transform: translateZ(0);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.prize-info {
|
||||||
|
position: absolute;
|
||||||
|
top: 100px;
|
||||||
|
right: 40px;
|
||||||
|
z-index: 100;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 15px;
|
||||||
|
align-items: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-text {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: bold;
|
||||||
|
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
|
||||||
|
padding: 10px 20px;
|
||||||
|
background: rgba(0, 0, 0, 0.3);
|
||||||
|
border-radius: 8px;
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.reset-button {
|
||||||
|
padding: 12px 24px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #fff;
|
||||||
|
background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
box-shadow: 0 4px 12px rgba(239, 68, 68, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.reset-button:hover {
|
||||||
|
background: linear-gradient(135deg, #dc2626 0%, #b91c1c 100%);
|
||||||
|
box-shadow: 0 6px 16px rgba(239, 68, 68, 0.6);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.reset-button:active {
|
||||||
|
transform: translateY(0);
|
||||||
|
box-shadow: 0 2px 8px rgba(239, 68, 68, 0.4);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 187 KiB |
|
Before Width: | Height: | Size: 233 KiB After Width: | Height: | Size: 280 KiB |
|
Before Width: | Height: | Size: 181 KiB After Width: | Height: | Size: 253 KiB |
|
Before Width: | Height: | Size: 669 KiB After Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 727 KiB After Width: | Height: | Size: 1.3 MiB |
63
woodpecker.yml
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
steps:
|
||||||
|
- name: pnpm-build
|
||||||
|
image: node:22-alpine
|
||||||
|
when:
|
||||||
|
event:
|
||||||
|
- push
|
||||||
|
- pull_request
|
||||||
|
- manual
|
||||||
|
branch:
|
||||||
|
- master
|
||||||
|
- develop
|
||||||
|
- main
|
||||||
|
commands: |
|
||||||
|
cd $CI_WORKSPACE
|
||||||
|
npm install -g pnpm
|
||||||
|
pnpm install
|
||||||
|
pnpm build
|
||||||
|
|
||||||
|
# 获取分支名
|
||||||
|
BRANCH_NAME=$(echo $CI_COMMIT_BRANCH | tr / -)
|
||||||
|
echo "Branch name: $BRANCH_NAME"
|
||||||
|
|
||||||
|
# 版本号: 分支名-package.json版本
|
||||||
|
PKG_VERSION=$(node -e "console.log(require('./package.json').version)")
|
||||||
|
PROJECT_VERSION="$BRANCH_NAME-$PKG_VERSION"
|
||||||
|
echo "Docker tag: $PROJECT_VERSION"
|
||||||
|
echo $PROJECT_VERSION > $CI_WORKSPACE/project_version.txt
|
||||||
|
|
||||||
|
- name: docker-build
|
||||||
|
image: docker:24.0.5-cli
|
||||||
|
when:
|
||||||
|
event:
|
||||||
|
- push
|
||||||
|
- pull_request
|
||||||
|
- manual
|
||||||
|
branch:
|
||||||
|
- master
|
||||||
|
- develop
|
||||||
|
- main
|
||||||
|
volumes:
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
commands: |
|
||||||
|
PROJECT_VERSION=$(cat $CI_WORKSPACE/project_version.txt)
|
||||||
|
MODULE_NAME=log-lottery
|
||||||
|
|
||||||
|
echo "Building Docker image: $MODULE_NAME:$PROJECT_VERSION"
|
||||||
|
|
||||||
|
cd $CI_WORKSPACE
|
||||||
|
|
||||||
|
docker build -t harbor.lnh2e.com/lingniu-v1/$MODULE_NAME:$PROJECT_VERSION .
|
||||||
|
|
||||||
|
mkdir -p /root/.docker
|
||||||
|
cat > /root/.docker/config.json <<EOF
|
||||||
|
{
|
||||||
|
"auths": {
|
||||||
|
"harbor.lnh2e.com": {
|
||||||
|
"auth": "$(echo Y2ljZDpMbkBjaWNkMDE=)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
docker push harbor.lnh2e.com/lingniu-v1/$MODULE_NAME:$PROJECT_VERSION
|
||||||