48 Commits

Author SHA1 Message Date
宋杰 4ffa3a5472 新增打包环境。 4 weeks ago
宋杰 6fe70f29c8 获取地址栏token。 4 weeks ago
宋杰 752b1ae951 从浏览器获取token。 4 weeks ago
宋杰 8f182e2624 合并代码 4 weeks ago
宋杰 1bffac237a Merge branch 'milestone-20250723-众筹' into milestone-20250722-众筹抽奖合并专用分支 4 weeks ago
pangluotong 909cc2fab0 Merge branch 'milestone-20250722-抽奖' into pangluotong/feature-20250712103401-抽奖 4 weeks ago
Ethereal fc15945e54 Merge branch 'wangyi/feature-20250710191445-抽奖' into milestone-20250722-抽奖 4 weeks ago
pangluotong 04fbe21a8c 更改登录页 4 weeks ago
Ethereal 9053349e7c 修改奖品列表逻辑 4 weeks ago
no99 120ecba0fa Merge branch 'hongxilin/feature-20250710175148-抽奖' into milestone-20250722-抽奖 4 weeks ago
no99 e7dc44ceb2 修改基础路径 4 weeks ago
pangluotong 52a56309c3 Merge branch 'milestone-20250722-抽奖' of http://39.101.133.168:8807/hongxilin/activityLink into pangluotong/feature-20250712103401-抽奖 4 weeks ago
pangluotong 2cfed1a5f5 奖品栏滑动, 增加滚动条 4 weeks ago
Ethereal 15bb193f9b Merge branch 'wangyi/feature-20250710191445-抽奖' into milestone-20250722-抽奖 4 weeks ago
Ethereal 824354753e 完成球体下移 4 weeks ago
Ethereal 38e7fb18b3 完成抽奖逻辑的优化 4 weeks ago
pangluotong 2c6cb78631 注释掉无关代码 4 weeks ago
pangluotong b8e54a74c7 Merge branch 'milestone-20250722-抽奖' of http://39.101.133.168:8807/hongxilin/activityLink into pangluotong/feature-20250712103401-抽奖 4 weeks ago
pangluotong 4645de18e2 箭头方向更改 4 weeks ago
Ethereal 812e9f3343 即将修改奖品列表 4 weeks ago
Ethereal 93ecc22a6b Merge branch 'milestone-20250722-抽奖' of http://39.101.133.168:8807/hongxilin/activityLink into wangyi/feature-20250710191445-抽奖 4 weeks ago
no99 afc6d6b6ca 解决冲突 4 weeks ago
Ethereal ee8578e25e 完成所有接口的测试 4 weeks ago
no99 95d651e559 解决冲突 4 weeks ago
no99 e1dc072626 Merge branch 'milestone-20250722-抽奖' of http://39.101.133.168:8807/hongxilin/activityLink into hongxilin/feature-20250710175148-抽奖 4 weeks ago
no99 d3f27f2973 Merge branch 'milestone-20250722-抽奖' of http://39.101.133.168:8807/hongxilin/activityLink into hongxilin/feature-20250710175148-抽奖 4 weeks ago
no99 9fc04a8fcd Merge branch 'hongxilin/feature-20250710175148-抽奖' into milestone-20250722-抽奖 4 weeks ago
Ethereal c0f0078056 最后的获奖列表还未测试 4 weeks ago
no99 a87d92a904 添加抽奖的接口 4 weeks ago
Ethereal 90c69d6e2e 仅剩获奖列表后端接口 4 weeks ago
pangluotong 70d6fd0bd3 Merge branch 'milestone-20250722-抽奖' of http://39.101.133.168:8807/hongxilin/activityLink into pangluotong/feature-20250712103401-抽奖 4 weeks ago
pangluotong 442867840f 完成闪烁 4 weeks ago
Ethereal 2873423dd4 完成文字闪烁 4 weeks ago
pangluotong 962e553bb6 更改镜像 4 weeks ago
pangluotong ab400ea238 解决冲突 4 weeks ago
pangluotong 18214e3901 Merge branch 'pangluotong/feature-20250712103401-抽奖' into milestone-20250722-抽奖 4 weeks ago
pangluotong 350c553243 庞-更改 4 weeks ago
Ethereal 3598947611 api修改 4 weeks ago
Ethereal 316876ce8d 样式适配+逻辑修复 4 weeks ago
no99 55c3d34dcc 一期终版 1 month ago
no99 913c0f7edc Merge branch 'milestone-20250722-抽奖' of http://39.101.133.168:8807/hongxilin/activityLink into hongxilin/feature-20250710175148-抽奖 1 month ago
no99 14c6cb3a3a 添加链接 1 month ago
no99 fd8987a08d 初版 1 month ago
Ethereal 35cec973a1 7.17推送,王毅更改抽奖卡牌,庞罗统更改奖品列表 1 month ago
pangluotong 555807b7c7 推出抽奖就要再次输入密码 1 month ago
pangluotong 537bb471fe 登录页 1 month ago
Ethereal b4d59301aa 完成大部分逻辑,先测验仓库命令 1 month ago
no99 62c92bc382 下载pinia 1 month ago
  1. 7
      .env.development
  2. 4
      .env.production
  3. 2
      .vercel/project.json
  4. 3
      .vscode/extensions.json
  5. 44
      CORS_README.md
  6. 4
      README.md
  7. 2
      index.html
  8. 1829
      package-lock.json
  9. 16
      package.json
  10. BIN
      public/img/edifier.jpg
  11. BIN
      public/img/huawei.png
  12. BIN
      public/img/ipad.jpg
  13. BIN
      public/img/kindle.jpg
  14. BIN
      public/img/mbp.jpg
  15. BIN
      public/img/secrit.jpg
  16. BIN
      public/img/spark.jpg
  17. 73
      src/api/API.js
  18. 14
      src/api/zhongchouApi.js
  19. 588
      src/assets/PrizePanel1.vue
  20. BIN
      src/assets/beijingtu.jpg
  21. BIN
      src/assets/bg@2x.png
  22. 11
      src/assets/css/animate.min.css
  23. BIN
      src/assets/daijiemi.png
  24. BIN
      src/assets/image.png
  25. BIN
      src/assets/img/bg.png
  26. BIN
      src/assets/img/展开.png
  27. BIN
      src/assets/img/待揭秘.png
  28. BIN
      src/assets/img/抽奖按钮.png
  29. BIN
      src/assets/img/麒麟.png
  30. BIN
      src/assets/loginback.png
  31. BIN
      src/assets/lottery
  32. BIN
      src/assets/music.mp3
  33. BIN
      src/assets/qilin.webp
  34. BIN
      src/assets/展开.png
  35. BIN
      src/assets/登录背景.png
  36. BIN
      src/data/music.mp3
  37. 21
      src/main.js
  38. 25
      src/router/index.js
  39. 81
      src/store/lottery.js
  40. 25
      src/stores/auth.js
  41. 56
      src/style.css
  42. 320
      src/utils/CSS3DRenderer.js
  43. 517
      src/utils/TrackballControls.js
  44. 55
      src/utils/config.js
  45. 260
      src/utils/prizeList.js
  46. 113
      src/utils/request.js
  47. 957
      src/utils/three.min.js
  48. 1
      src/utils/tween.min.js
  49. 101
      src/views/choujiang/Login.vue
  50. 2153
      src/views/choujiang/hxl-cj/cj.vue
  51. 347
      src/views/choujiang/index.vue
  52. 185
      src/views/choujiang/lottery/CardItem.vue
  53. 78
      src/views/choujiang/lottery/ControlBar.vue
  54. 886
      src/views/choujiang/lottery/Lottery3D.vue
  55. 54
      src/views/choujiang/lottery/Mascot.vue
  56. 76
      src/views/choujiang/lottery/MusicPlayer.vue
  57. 716
      src/views/choujiang/lottery/PrizePanel.vue
  58. 37
      src/views/choujiang/lottery/Qipao.vue
  59. 62
      src/views/choujiang/lottery/UserList.vue
  60. 217
      src/views/choujiang/lottery/dataManager.js
  61. 198
      src/views/choujiang/lottery/lotteryEngine.js
  62. 2
      src/views/homePage.vue
  63. 22
      src/views/zhongchou/index.vue
  64. 42
      vite.config.js
  65. 22
      vite.config.js.timestamp-1753077096347-78067037e45f7.mjs
  66. 98
      修改完成总结.md
  67. 92
      抽奖逻辑修改说明.md
  68. 113
      测试用例.md

7
.env.development

@ -2,13 +2,10 @@
VITE_ENV = 'development'
VITE_OUTPUT_DIR = 'dev'
# public path
VITE_PUBLIC_PATH = /
VITE_PUBLIC_PATH = /test
#新数据接口
# VITE_APP_API_BASE_URL = "http://39.101.133.168:8828/link"
# VITE_APP_API_BASE_URL = "http://47.92.148.30:3003/mock/3267"
# VITE_APP_API_BASE_URL = "http://dbqb.nfdxy.net/devLotApi"
VITE_APP_API_BASE_URL = "https://dbqb.nfdxy.net/devLotApi"
# Whether to open mock

4
.env.production

@ -2,14 +2,14 @@
VITE_ENV = 'production'
VITE_OUTPUT_DIR = 'dist'
# public path
VITE_PUBLIC_PATH = /aixiaocaishen
VITE_PUBLIC_PATH = /
# VITE_PUBLIC_PATH = /
# Whether to open mock
VITE_USE_MOCK = true
#新数据接口
# VITE_APP_API_BASE_URL = https://api.homilychart.com/link
VITE_APP_API_BASE_URL = https://dbqb.nfdxy.net/devLotApi
# Whether to enable gzip or brotli compression
# Optional: gzip | brotli | none

2
.vercel/project.json

@ -1 +1 @@
{"projectName":"trae_zf0uu3bt"}
{"projectName":"trae_zf0uu3bt"}

3
.vscode/extensions.json

@ -1,3 +0,0 @@
{
"recommendations": ["Vue.volar"]
}

44
CORS_README.md

@ -0,0 +1,44 @@
# 跨域问题解决方案
## 配置说明
本项目已配置Vite代理来解决跨域问题。
### 1. Vite配置 (vite.config.js)
```javascript
server: {
host: '0.0.0.0',
port: 3000,
proxy: {
'/api': {
target: 'https://dbqb.nfdxy.net',
changeOrigin: true,
secure: false,
rewrite: (path) => path.replace(/^\/api/, '/devLotApi/api')
}
}
}
```
### 2. 请求配置 (src/utils/request.js)
- `baseURL: '/api'` - 设置基础URL为代理路径
### 3. API调用 (src/api/API.js)
- 使用相对路径 `/prize/list` 而不是完整的URL
- 实际请求会被代理到 `https://dbqb.nfdxy.net/devLotApi/api/prize/list`
## 工作原理
1. 前端发起请求到 `/api/prize/list`
2. Vite开发服务器拦截请求
3. 代理将请求转发到 `https://dbqb.nfdxy.net/devLotApi/api/prize/list`
4. 服务器响应通过代理返回给前端
## 注意事项
- 此配置仅在开发环境有效
- 生产环境需要在服务器端配置CORS或使用nginx代理
- 确保目标服务器允许跨域请求

4
README.md

@ -7,4 +7,6 @@ Learn more about IDE Support for Vue in the [Vue Docs Scaling up Guide](https://
npm install
npm install vue-router
npm install axios
npm install element-plus --save
npm install element-plus --save
npm install pinia
npm install three

2
index.html

@ -7,6 +7,8 @@
<title>Vite + Vue</title>
</head>
<body>
<script src="/src/utils/tween.min.js"></script>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>

1829
package-lock.json
File diff suppressed because it is too large
View File

16
package.json

@ -6,14 +6,24 @@
"scripts": {
"dev": "vite --host",
"build": "vite build",
"preview": "vite preview"
"preview": "vite preview",
"build:test": "vite build --mode test",
"build:prod": "vite build --mode production"
},
"dependencies": {
"@tweenjs/tween.js": "^18.6.4",
"@vitejs/plugin-vue": "^4.6.2",
"axios": "^1.10.0",
"element-plus": "^2.10.4",
"element-plus": "^2.10.3",
"pinia": "^3.0.3",
"pinia-plugin-persistedstate": "^4.4.1",
"three": "^0.178.0",
"vite": "^4.5.3",
"vue": "^3.5.17",
"vue-router": "^4.5.1"
},
"devDependencies": {
"unplugin-auto-import": "^19.3.0",
"unplugin-vue-components": "^28.8.0"
}
}
}

BIN
public/img/edifier.jpg

After

Width: 198  |  Height: 198  |  Size: 23 KiB

BIN
public/img/huawei.png

After

Width: 200  |  Height: 200  |  Size: 49 KiB

BIN
public/img/ipad.jpg

After

Width: 220  |  Height: 220  |  Size: 44 KiB

BIN
public/img/kindle.jpg

After

Width: 289  |  Height: 289  |  Size: 42 KiB

BIN
public/img/mbp.jpg

After

Width: 220  |  Height: 220  |  Size: 6.9 KiB

BIN
public/img/secrit.jpg

After

Width: 224  |  Height: 224  |  Size: 12 KiB

BIN
public/img/spark.jpg

After

Width: 260  |  Height: 260  |  Size: 37 KiB

73
src/api/API.js

@ -1,19 +1,56 @@
// import request from "../utils/request";
// const APIurl = import.meta.env.VITE_APP_API_BASE_URL;
// // 开发环境使用代理,生产环境使用环境变量
// // const APIurl = import.meta.env.DEV ? '/Api' : import.meta.env.VITE_APP_API_BASE_URL;
import axios from "axios";
import request from "../utils/request";
// export function addRecordAPI(data) {
// return request({
// url: `Api/api/funding/addRecord`,
// method: 'post',
// data: data
// })
// }
// // 新增:获取活动信息接口
// export function getActivity1API() {
// return request({
// url: `Api/api/funding/getActivity1`,
// method: 'post'
// })
// }
const APIurl = import.meta.env.VITE_APP_API_BASE_URL;
// 查询所有奖品和对应等级,按sort排序
export const getPrizeListApi = function (params) {
return request({
url: `${APIurl}/api/prize/list`,
method: "POST",
data: new URLSearchParams(params),
});
};
// 查询用户
export const getUserListApi = function (params) {
return request({
url: `${APIurl}/api/user/list`,
method: "POST",
data: new URLSearchParams(params),
});
};
// 查询中奖名单
export const getGetPrizeUserListApi = function (params) {
return request({
url: `${APIurl}/admin/win/list`,
method: "POST",
data: params,
});
};
// 开始抽奖
export const startLotteryApi = function (params) {
return request({
url: `${APIurl}/api/lottery/start`,
method: "POST",
data: params,
});
};
// 新增:每轮抽奖接口
export function drawLottery(data) {
return request({
url: `${APIurl}/api/lottery/start`,
method: "post",
data: {
gradeName: data.gradeName,
prizeName: data.prizeName,
perWin: data.perWin,
remainNum: data.remainNum,
gradeId: data.gradeId,
prizeId: data.prizeId,
},
});
}

14
src/api/zhongchouApi.js

@ -5,15 +5,21 @@ const APIurl = import.meta.env.VITE_APP_API_BASE_URL;
export function addRecordAPI(data) {
return request({
url: `Api/api/funding/addRecord`,
url: `${APIurl}/api/funding/addRecord`,
method: 'post',
data: data
data: data,
headers: {
token: localStorage.getItem('localToken')
},
})
}
// 新增:获取活动信息接口
export function getActivity1API() {
return request({
url: `Api/api/funding/getActivity`,
method: 'post'
url: `${APIurl}/api/funding/getActivity`,
method: 'post',
headers: {
token: localStorage.getItem('localToken')
},
})
}

588
src/assets/PrizePanel1.vue

@ -0,0 +1,588 @@
<template>
<div class="prize-panel-root">
<div class="prize-panel-list" v-if="prizes && prizes.length" :style="containerStyle">
<div
class="prize-panel-item"
v-for="(prize, idx) in prizes"
:key="prize.type || idx"
:class="{
'revealed-highlight': idx === lastRevealedIdx,
'winner-mode-highlight': showWinnerList && idx === lastRevealedIdx
}"
@click="showWinnerList ? null : handleReveal(idx)"
:style="{ cursor: showWinnerList ? 'default' : 'pointer' }"
:ref="el => setPrizeRef(el, idx)"
v-show="!shouldHideOtherPrizes || idx === lastRevealedIdx"
>
<div v-if="isRevealed(idx)" class="prize-card">
<div class="prize-img-wrap">
<img class="prize-img" :src="prize.img" :alt="prize.title" />
</div>
<div class="prize-info">
<div class="prize-row prize-row-top">
<span class="prize-level">{{ prize.title }}</span>
<span class="prize-name">{{ prize.text }}</span>
</div>
<div class="prize-row prize-row-bottom">
<div class="progress-bar-bg">
<div
class="progress-bar-fill"
:style="{ width:getProgressPercent(prize) + '%' }"
></div>
<span class="progress-bar-text">
{{ prize.count-getLeftCount(prize) }}/{{ prize.count }}
</span>
</div>
</div>
</div>
</div>
<div v-else class="prize-card prize-card-mask">
<img src="../../../assets/daijiemi.png" alt="待揭秘" class="prize-mask-img" />
</div>
</div>
<!-- 隐藏占位div -->
<div v-show="!shouldHideOtherPrizes"></div>
<div v-show="!shouldHideOtherPrizes"></div>
<div v-show="!shouldHideOtherPrizes"></div>
<div v-show="!shouldHideOtherPrizes"></div>
<!-- 动态定位的获奖名单按钮 -->
<div
class="prize-panel-footer"
:class="{ 'winner-mode': shouldHideOtherPrizes }"
:style="winnerBtnStyle"
>
<div class="arrow-up" @click="openWinnerList"></div>
<button ref="winnerBtnRef" class="winner-btn" @click="toggleWinnerList">
{{ showWinnerList ? '关闭名单' : '获奖名单' }}
</button>
<div
v-if="showWinnerList"
class="winner-modal-mask"
@click="closeWinnerList"
>
<div
class="winner-modal"
:style="{ position: 'absolute', left: modalLeft + 'px', top: modalTop + 'px' }"
@click.stop
>
<div class="winner-modal-header">
<div class="winner-modal-title">Homily ID</div>
<div class="winner-modal-close" @click="closeWinnerList">×</div>
</div>
<ul class="winner-list">
<li v-for="(user, idx) in fakeWinners" :key="idx">
<!-- <span>{{ user.id }}</span> - <span>{{ user.name }}</span> - -->
<span>{{ user.id }}</span>
<span>{{ user.prize }}</span>
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, nextTick, watch } from "vue";
const props = defineProps({
prizes: Array,
});
//
const revealedCount = ref(0);
//
const lastRevealedIdx = ref(-1);
//
const prizeRefs = ref([]);
//
const winnerBtnStyle = ref({
position: 'absolute',
left: '0',
bottom: '0',
width: '100%'
});
//
const containerStyle = computed(() => {
if (shouldHideOtherPrizes.value) {
return {
justifyContent: 'flex-start',
alignItems: 'flex-start',
paddingTop: '20px'
};
}
return {};
});
//
function setPrizeRef(el, idx) {
if (el) {
prizeRefs.value[idx] = el;
}
}
//
const isRevealed = idx => idx >= (props.prizes?.length || 0) - revealedCount.value;
// index
const nextRevealIdx = computed(() => (props.prizes?.length || 0) - revealedCount.value - 1);
//
function handleReveal(idx) {
if (idx === nextRevealIdx.value) {
revealedCount.value++;
lastRevealedIdx.value = idx; //
}
}
//
function getLeftCount(prize) {
// type dataManager.state.basicData.luckyUsers
// luckyUsers 访
// window.dataManager
let luckyUsers =
(window.dataManager && window.dataManager.state.basicData.luckyUsers) || {};
let got = luckyUsers[prize.type]?.length || 0;
return prize.count - got;
}
//
const showWinnerList = ref(false);
const fakeWinners = ref([
{ id: "90044065", name: "张三", prize: "六等奖" },
{ id: "90044066", name: "李四", prize: "六等奖" },
{ id: "90044067", name: "王五", prize: "六等奖" },
{ id: "90044068", name: "赵六", prize: "六等奖" },
{ id: "90044069", name: "小明", prize: "六等奖" },
]);
//
const shouldHideOtherPrizes = computed(() => {
return showWinnerList.value && lastRevealedIdx.value >= 0;
});
//
function positionWinnerBtn() {
if (lastRevealedIdx.value >= 0 && prizeRefs.value[lastRevealedIdx.value]) {
const highlightedPrize = prizeRefs.value[lastRevealedIdx.value];
//
if (shouldHideOtherPrizes.value) {
const rect = highlightedPrize.getBoundingClientRect();
const containerRect = highlightedPrize.parentElement.getBoundingClientRect();
//
const relativeTop = rect.bottom - containerRect.top + 18; // 18px
winnerBtnStyle.value = {
position: 'absolute',
left: '0',
top: relativeTop + 'px',
width: '100%',
zIndex: '20'
};
} else {
//
winnerBtnStyle.value = {
position: 'absolute',
left: '0',
bottom: '0',
width: '100%'
};
}
} else {
//
winnerBtnStyle.value = {
position: 'absolute',
left: '0',
bottom: '0',
width: '100%'
};
}
}
function openWinnerList() {
showWinnerList.value = true;
//
if (lastRevealedIdx.value >= 0) {
nextTick(() => {
positionWinnerBtn();
});
} else {
//
winnerBtnStyle.value = {
position: 'absolute',
left: '0',
bottom: '0',
width: '100%'
};
}
}
function closeWinnerList() {
showWinnerList.value = false;
//
nextTick(() => {
winnerBtnStyle.value = {
position: 'absolute',
left: '0',
bottom: '0',
width: '100%'
};
});
}
const winnerBtnRef = ref(null);
const modalLeft = ref(0);
const modalTop = ref(0);
function toggleWinnerList() {
showWinnerList.value = !showWinnerList.value;
if (showWinnerList.value) {
nextTick(() => {
const btn = winnerBtnRef.value;
if (btn) {
const rect = btn.getBoundingClientRect();
modalLeft.value = rect.left-22;
modalTop.value = rect.bottom + 20; // 4px
}
});
}
}
//
watch(lastRevealedIdx, () => {
nextTick(() => {
//
if (lastRevealedIdx.value >= 0 && showWinnerList.value) {
positionWinnerBtn();
} else if (lastRevealedIdx.value >= 0) {
//
winnerBtnStyle.value = {
position: 'absolute',
left: '0',
bottom: '0',
width: '100%'
};
}
});
});
function getProgressPercent(prize) {
const total = prize.count || 1;
const left = getLeftCount(prize);
const got = total - left;
return Math.round((got / total) * 100);
}
</script>
<style scoped>
.prize-panel-list {
position: absolute;
top: 20px;
left: 20px;
background: none;
z-index: 10;
min-width: 320px;
text-align: left;
display: flex;
flex-direction: column;
gap: 18px;
/* 新增:支持flexbox排序 */
align-items: flex-start;
/* 新增:支持滚动和定位 */
max-height: calc(100vh - 40px);
/* overflow-y: auto; */
/* 新增:当显示获奖名单时的特殊样式 */
transition: all 0.3s ease;
}
.prize-panel-item {
background: #ffd283;
border-radius: 6px 6px 6px 6px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
display: flex;
align-items: center;
min-width: 300px;
transition: opacity 0.3s ease, transform 0.3s ease;
}
.prize-card {
display: flex;
align-items: center;
width: 100%;
padding: 10px 18px;
}
.prize-img-wrap {
width: 64px;
height: 64px;
border-radius: 50%;
background: #fff;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
margin-right: 18px;
border: 2px solid #fff3e0;
}
.prize-img {
width: 60px;
height: 60px;
object-fit: contain;
}
.prize-info {
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
}
.prize-row {
display: flex;
align-items: center;
background: #ffffff;
border-radius: 93px 93px 93px 93px;
}
.prize-row-top {
margin-bottom: 8px;
}
.prize-level {
background: linear-gradient(90deg, #ff9800 0%, #ff5722 100%);
color: #fff;
border-radius: 15.71px 15.71px 15.71px 15.71px;
padding: 2px 18px;
font-size: 18px;
font-weight: bold;
margin-right: 12px;
}
.prize-name {
font-size: 18px;
color: #d84315;
font-weight: 500;
}
/* .prize-row-bottom {
background: linear-gradient(90deg, #ff9800 0%, #ff5722 100%);
background: #8a3500;
border-radius: 16px;
color: #fff;
font-size: 20px;
font-weight: bold;
padding: 2px 0 2px 0;
justify-content: center;
min-width: 80px;
} */
.prize-count {
font-size: 20px;
font-weight: bold;
}
.prize-divider {
margin: 0 4px;
font-size: 20px;
}
.prize-total {
font-size: 20px;
font-weight: bold;
}
/* 新增:获奖名单按钮容器样式调整 */
.prize-panel-footer {
position: absolute;
left: 0;
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
z-index: 20;
transition: all 0.3s ease;
/* 移除 bottom 定位,改为动态定位 */
}
/* 新增:获奖名单模式下的按钮样式 */
.prize-panel-footer.winner-mode {
position: relative;
margin-top: 18px;
}
.arrow-up {
width: 36px;
height: 24px;
background: url("@/assets/arrow-up.svg") no-repeat center/contain;
margin-bottom: 4px;
cursor: pointer;
}
.winner-btn {
background: rgba(255, 210, 131, 0.8);
color: #fff;
border: #fff;
border-radius: 8px;
padding: 15px 79px;
font-size: 20px;
font-weight: bold;
cursor: pointer;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
transition: all 0.3s ease;
}
.winner-btn:hover {
background: rgba(255, 210, 131, 1);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.winner-modal-mask {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: rgba(0,0,0,0.01);
z-index: 1000;
display: flex;
align-items: flex-start;
justify-content: center;
}
.winner-modal {
background:rgba(255, 210, 131, 0.8);
border-radius: 12px;
/* margin-top: 2vh; */
padding-top: 12px;
min-width: 280px;
max-width: 90vw;
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.12);
position: relative;
}
.winner-modal-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 18px;
padding: 0 12px;
}
.winner-modal-title {
font-size: 22px;
font-weight: bold;
text-align: center;
flex: 1;
}
.winner-modal-close {
font-size: 24px;
color: #d84315;
cursor: pointer;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
background: rgba(255, 255, 255, 0.8);
transition: all 0.2s ease;
}
.winner-modal-close:hover {
background: rgba(255, 255, 255, 1);
transform: scale(1.1);
}
.winner-list {
max-height: 260px;
/* background: rgba(255, 210, 131, 0.8);/ */
overflow-y: auto;
padding: 0;
margin: 0;
list-style: none;
}
.winner-list li {
padding: 8px 0;
/* border-bottom: 1px solid #f2f2f2; */
font-size: 17px;
color: #d84315;
display: flex;
gap: 12px;
align-items: center;
justify-content: center;
text-align: center;
}
.progress-bar-bg {
position: relative;
width: 220px;
height: 28px;
background: #E9620E;
border-radius: 16px;
overflow: hidden;
display: flex;
align-items: center;
margin: 0 auto;
border: #E13726;
}
.progress-bar-fill {
position: absolute;
left: 0;
top: 0;
height: 100%;
/* background: linear-gradient(90deg, #ff9800 0%, #8a3500 100%); */
background: #8a3500;
border-radius: 16px;
transition: width 0.4s;
z-index: 1;
}
.progress-bar-text {
position: relative;
width: 100%;
text-align: center;
color: #ffffff;
font-size: 18px;
font-weight: bold;
z-index: 2;
letter-spacing: 1px;
}
.prize-card-mask {
position: relative;
width: 342px;
height: 88px;
display: flex;
/* align-items: center;
justify-content: center; */
padding: 0;
overflow: hidden;
}
.prize-mask-img {
object-fit: cover;
position: absolute;
width: 100%;
height: 98%;
object-fit: cover;
left: 0;
top: 0;
border-radius: 8px 8px 8px 8px;
}
.prize-panel-item.revealed-highlight {
border: 3px solid #ff9800;
box-shadow: 0 0 16px 4px #ff9800aa;
transform: scale(1.05);
z-index: 2;
transition: all 0.3s;
/* 确保高亮奖品在顶部 */
position: relative;
}
/* 新增:只有在获奖名单模式下才上移 */
.prize-panel-item.revealed-highlight.winner-mode-highlight {
order: -1;
margin-bottom: 18px;
/* 确保在左侧顶部显示 */
align-self: flex-start;
width: 100%;
}
/* 新增:获奖名单模式下的高亮样式 */
.prize-panel-item.winner-mode-highlight {
transform: scale(1.05);
box-shadow: 0 0 24px 8px #ff9800dd;
border: 4px solid #ff9800;
animation: winnerPulse 2s ease-in-out infinite;
/* 确保在左侧顶部显示 */
margin-top: 0;
margin-bottom: 18px;
}
@keyframes winnerPulse {
0%, 100% {
box-shadow: 0 0 24px 8px #ff9800dd;
}
50% {
box-shadow: 0 0 32px 12px #ff9800ff;
}
}
</style>

BIN
src/assets/beijingtu.jpg

After

Width: 3844  |  Height: 2156  |  Size: 1.9 MiB

BIN
src/assets/bg@2x.png

After

Width: 3840  |  Height: 2160  |  Size: 5.6 MiB

11
src/assets/css/animate.min.css
File diff suppressed because it is too large
View File

BIN
src/assets/daijiemi.png

After

Width: 766  |  Height: 220  |  Size: 144 KiB

BIN
src/assets/image.png

After

Width: 664  |  Height: 199  |  Size: 83 KiB

BIN
src/assets/img/bg.png

After

Width: 1920  |  Height: 1080  |  Size: 1.8 MiB

BIN
src/assets/img/展开.png

After

Width: 40  |  Height: 34  |  Size: 2.9 KiB

BIN
src/assets/img/待揭秘.png

After

Width: 383  |  Height: 110  |  Size: 47 KiB

BIN
src/assets/img/抽奖按钮.png

After

Width: 259  |  Height: 95  |  Size: 8.1 KiB

BIN
src/assets/img/麒麟.png

After

Width: 252  |  Height: 335  |  Size: 104 KiB

BIN
src/assets/loginback.png

After

Width: 1920  |  Height: 1080  |  Size: 1.7 MiB

BIN
src/assets/lottery

BIN
src/assets/music.mp3

BIN
src/assets/qilin.webp

BIN
src/assets/展开.png

After

Width: 40  |  Height: 34  |  Size: 2.9 KiB

BIN
src/assets/登录背景.png

After

Width: 5760  |  Height: 3240  |  Size: 867 KiB

BIN
src/data/music.mp3

21
src/main.js

@ -1,6 +1,17 @@
import { createApp } from 'vue'
// import './style.css'
import App from './App.vue'
import router from './router'
import { createApp } from "vue";
import { createPinia } from "pinia";
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate';
import "./style.css";
import App from "./App.vue";
import router from "./router";
import ElementPlus from "element-plus";
createApp(App).use(router).mount('#app')
const pinia = createPinia();
pinia.use(piniaPluginPersistedstate);
const app = createApp(App);
app.use(router);
app.use(ElementPlus);
app.use(pinia);
app.mount("#app");

25
src/router/index.js

@ -1,4 +1,5 @@
import { createRouter, createWebHistory } from 'vue-router'
import { useAuthStore } from '../stores/auth';
const routes = [
{
path: '/',
@ -21,6 +22,16 @@ const routes = [
path: '/zhongchou',
name: 'zhongchou',
component: () => import('../views/zhongchou/index.vue'),
},
{
path: '/hxlCj',
name: 'hxlCj',
component: () => import('../views/choujiang/hxl-cj/cj.vue'),
},
{
path: '/login',
name: 'login',
component: () => import('../views/choujiang/Login.vue'),
}
]
// 创建路由实例
@ -28,5 +39,19 @@ const router = createRouter({
history: createWebHistory(import.meta.env.VITE_PUBLIC_PATH),
routes
})
// 添加路由守卫
router.beforeEach((to, from, next) => {
const authStore = useAuthStore(); // 获取auth store实例
// 仅对/choujiang路由进行登录验证
if (to.path === '/choujiang') {
if (!authStore.isLoggedIn) {
// 如果未登录,重定向到登录页面
next('/login');
return;
}
}
next();
})
// 导出
export default router

81
src/store/lottery.js

@ -0,0 +1,81 @@
// activityLink/src/store/lottery.js
import { defineStore } from 'pinia'
import { ref } from 'vue'
export const useLotteryStore = defineStore('lottery', () => {
const lotteryState = ref('idle') // idle, ready, rotating, result
const lastRevealedIdx = ref(-1)
const waitingForNextReveal = ref(false)
//设置中奖人数列表
const winners = ref([])
// 添加用户列表管理
const allUsers = ref([])
const isUsersLoaded = ref(false)
const winnerList = ref([])
function setWinners(list) {
// 如果是数组,则添加到现有数组中;如果是单个项目,则直接添加
if (Array.isArray(list)) {
winners.value = [...winners.value, ...list]
} else {
winners.value = [...winners.value, list]
}
}
function clearWinners() {
winners.value = []
}
function setLotteryState(state) {
lotteryState.value = state
}
function setLastRevealedIdx(idx) {
lastRevealedIdx.value = idx
}
function setWaitingForNextReveal(val) {
waitingForNextReveal.value = val
}
// 设置用户列表
function setAllUsers(users) {
allUsers.value = users
isUsersLoaded.value = true
}
// 获取随机用户名称
function getRandomUserName() {
if (allUsers.value.length > 0) {
const randomIndex = Math.floor(Math.random() * allUsers.value.length)
return allUsers.value[randomIndex]
}
return ""
}
function setWinnerList(list) {
winnerList.value = list
}
return {
lotteryState,
setLotteryState,
lastRevealedIdx,
setLastRevealedIdx,
waitingForNextReveal,
setWaitingForNextReveal,
winners,
setWinners,
clearWinners,
allUsers,
isUsersLoaded,
setAllUsers,
getRandomUserName,
winnerList,
setWinnerList
}
})

25
src/stores/auth.js

@ -0,0 +1,25 @@
import { defineStore } from 'pinia';
import { ref } from 'vue';
export const useAuthStore = defineStore('auth', () => {
// 登录状态
const isLoggedIn = ref(false);
// 登录方法
const login = () => {
isLoggedIn.value = true;
};
// 登出方法
const logout = () => {
isLoggedIn.value = false;
};
return {
isLoggedIn,
login,
logout
};
},{
persist: {
storage: sessionStorage, // 使用会话存储替代默认的localStorage
paths: ['isLoggedIn'] // 可选:指定需要持久化的状态字段
}
});
//开启持久化)

56
src/style.css

@ -1,26 +1,4 @@
/* :root {
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
}
body {
margin: 0;
@ -35,20 +13,6 @@ h1 {
line-height: 1.1;
}
button {
border-radius: 8px;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
background-color: #1a1a1a;
cursor: pointer;
transition: border-color 0.25s;
}
button:hover {
border-color: #646cff;
}
button:focus,
button:focus-visible {
outline: 4px auto -webkit-focus-ring-color;
@ -57,23 +21,3 @@ button:focus-visible {
.card {
padding: 2em;
}
#app {
margin: 0 auto;
display: flex;
width: 100%;
height: auto;
}
@media (prefers-color-scheme: light) {
:root {
color: #213547;
background-color: #ffffff;
}
a:hover {
color: #747bff;
}
button {
background-color: #f9f9f9;
}
} */

320
src/utils/CSS3DRenderer.js

@ -0,0 +1,320 @@
/**
* Based on http://www.emagix.net/academic/mscs-project/item/camera-sync-with-css3-and-webgl-threejs
* @author mrdoob / http://mrdoob.com/
* @author yomotsu / https://yomotsu.net/
*/
import * as THREE from "three";
export class CSS3DObject extends THREE.Object3D {
constructor(element) {
super();
this.element = element;
this.element.style.position = "absolute";
this.addEventListener("removed", function () {
if (this.element.parentNode !== null) {
this.element.parentNode.removeChild(this.element);
}
});
}
}
export class CSS3DSprite extends CSS3DObject {
constructor(element) {
super(element);
}
}
//
export function CSS3DRenderer() {
console.log("THREE.CSS3DRenderer", THREE.REVISION);
var _width, _height;
var _widthHalf, _heightHalf;
var matrix = new THREE.Matrix4();
var cache = {
camera: { fov: 0, style: "" },
objects: new WeakMap(),
};
var domElement = document.createElement("div");
domElement.style.overflow = "hidden";
this.domElement = domElement;
var cameraElement = document.createElement("div");
cameraElement.style.WebkitTransformStyle = "preserve-3d";
cameraElement.style.transformStyle = "preserve-3d";
domElement.appendChild(cameraElement);
var isIE = /Trident/i.test(navigator.userAgent);
this.getSize = function () {
return {
width: _width,
height: _height,
};
};
this.setSize = function (width, height) {
_width = width;
_height = height;
_widthHalf = _width / 2;
_heightHalf = _height / 2;
domElement.style.width = width + "px";
domElement.style.height = height + "px";
cameraElement.style.width = width + "px";
cameraElement.style.height = height + "px";
};
function epsilon(value) {
return Math.abs(value) < 1e-10 ? 0 : value;
}
function getCameraCSSMatrix(matrix) {
var elements = matrix.elements;
return (
"matrix3d(" +
epsilon(elements[0]) +
"," +
epsilon(-elements[1]) +
"," +
epsilon(elements[2]) +
"," +
epsilon(elements[3]) +
"," +
epsilon(elements[4]) +
"," +
epsilon(-elements[5]) +
"," +
epsilon(elements[6]) +
"," +
epsilon(elements[7]) +
"," +
epsilon(elements[8]) +
"," +
epsilon(-elements[9]) +
"," +
epsilon(elements[10]) +
"," +
epsilon(elements[11]) +
"," +
epsilon(elements[12]) +
"," +
epsilon(-elements[13]) +
"," +
epsilon(elements[14]) +
"," +
epsilon(elements[15]) +
")"
);
}
function getObjectCSSMatrix(matrix, cameraCSSMatrix) {
var elements = matrix.elements;
var matrix3d =
"matrix3d(" +
epsilon(elements[0]) +
"," +
epsilon(elements[1]) +
"," +
epsilon(elements[2]) +
"," +
epsilon(elements[3]) +
"," +
epsilon(-elements[4]) +
"," +
epsilon(-elements[5]) +
"," +
epsilon(-elements[6]) +
"," +
epsilon(-elements[7]) +
"," +
epsilon(elements[8]) +
"," +
epsilon(elements[9]) +
"," +
epsilon(elements[10]) +
"," +
epsilon(elements[11]) +
"," +
epsilon(elements[12]) +
"," +
epsilon(elements[13]) +
"," +
epsilon(elements[14]) +
"," +
epsilon(elements[15]) +
")";
if (isIE) {
return (
"translate(-50%,-50%)" +
"translate(" +
_widthHalf +
"px," +
_heightHalf +
"px)" +
cameraCSSMatrix +
matrix3d
);
}
return "translate(-50%,-50%)" + matrix3d;
}
function renderObject(object, camera, cameraCSSMatrix) {
if (object instanceof CSS3DObject) {
var style;
if (object instanceof CSS3DSprite) {
// http://swiftcoder.wordpress.com/2008/11/25/constructing-a-billboard-matrix/
matrix.copy(camera.matrixWorldInverse);
matrix.transpose();
matrix.copyPosition(object.matrixWorld);
matrix.scale(object.scale);
matrix.elements[3] = 0;
matrix.elements[7] = 0;
matrix.elements[11] = 0;
matrix.elements[15] = 1;
style = getObjectCSSMatrix(matrix, cameraCSSMatrix);
} else {
style = getObjectCSSMatrix(object.matrixWorld, cameraCSSMatrix);
}
var element = object.element;
var cachedObject = cache.objects.get(object);
if (cachedObject === undefined || cachedObject.style !== style) {
element.style.WebkitTransform = style;
element.style.transform = style;
var objectData = { style: style };
if (isIE) {
objectData.distanceToCameraSquared = getDistanceToSquared(
camera,
object
);
}
cache.objects.set(object, objectData);
}
if (element.parentNode !== cameraElement) {
cameraElement.appendChild(element);
}
}
for (var i = 0, l = object.children.length; i < l; i++) {
renderObject(object.children[i], camera, cameraCSSMatrix);
}
}
var getDistanceToSquared = (function () {
var a = new THREE.Vector3();
var b = new THREE.Vector3();
return function (object1, object2) {
a.setFromMatrixPosition(object1.matrixWorld);
b.setFromMatrixPosition(object2.matrixWorld);
return a.distanceToSquared(b);
};
})();
function filterAndFlatten(scene) {
var result = [];
scene.traverse(function (object) {
if (object instanceof THREE.CSS3DObject) result.push(object);
});
return result;
}
function zOrder(scene) {
var sorted = filterAndFlatten(scene).sort(function (a, b) {
var distanceA = cache.objects.get(a).distanceToCameraSquared;
var distanceB = cache.objects.get(b).distanceToCameraSquared;
return distanceA - distanceB;
});
var zMax = sorted.length;
for (var i = 0, l = sorted.length; i < l; i++) {
sorted[i].element.style.zIndex = zMax - i;
}
}
this.render = function (scene, camera) {
var fov = camera.projectionMatrix.elements[5] * _heightHalf;
if (cache.camera.fov !== fov) {
if (camera.isPerspectiveCamera) {
domElement.style.WebkitPerspective = fov + "px";
domElement.style.perspective = fov + "px";
}
cache.camera.fov = fov;
}
scene.updateMatrixWorld();
if (camera.parent === null) camera.updateMatrixWorld();
if (camera.isOrthographicCamera) {
var tx = -(camera.right + camera.left) / 2;
var ty = (camera.top + camera.bottom) / 2;
}
var cameraCSSMatrix = camera.isOrthographicCamera
? "scale(" +
fov +
")" +
"translate(" +
epsilon(tx) +
"px," +
epsilon(ty) +
"px)" +
getCameraCSSMatrix(camera.matrixWorldInverse)
: "translateZ(" +
fov +
"px)" +
getCameraCSSMatrix(camera.matrixWorldInverse);
var style =
cameraCSSMatrix + "translate(" + _widthHalf + "px," + _heightHalf + "px)";
if (cache.camera.style !== style && !isIE) {
cameraElement.style.WebkitTransform = style;
cameraElement.style.transform = style;
cache.camera.style = style;
}
renderObject(scene, camera, cameraCSSMatrix);
if (isIE) {
// IE10 and 11 does not support 'preserve-3d'.
// Thus, z-order in 3D will not work.
// We have to calc z-order manually and set CSS z-index for IE.
// FYI: z-index can't handle object intersection
zOrder(scene);
}
};
}

517
src/utils/TrackballControls.js

@ -0,0 +1,517 @@
import * as THREE from 'three';
// 创建 TrackballControls 类并导出
export class TrackballControls extends THREE.EventDispatcher {
constructor(object, domElement) {
super(); // 调用父类构造函数
var _this = this;
var STATE = { NONE: -1, ROTATE: 0, ZOOM: 1, PAN: 2, TOUCH_ROTATE: 3, TOUCH_ZOOM_PAN: 4 };
this.object = object;
this.domElement = (domElement !== undefined) ? domElement : document;
// API
this.enabled = true;
this.screen = { left: 0, top: 0, width: 0, height: 0 };
this.rotateSpeed = 1.0;
this.zoomSpeed = 1.2;
this.panSpeed = 0.3;
this.noRotate = false;
this.noZoom = false;
this.noPan = false;
this.staticMoving = false;
this.dynamicDampingFactor = 0.2;
this.minDistance = 0;
this.maxDistance = Infinity;
this.keys = [65 /*A*/, 83 /*S*/, 68 /*D*/];
// internals
this.target = new THREE.Vector3();
var EPS = 0.000001;
var lastPosition = new THREE.Vector3();
var _state = STATE.NONE,
_prevState = STATE.NONE,
_eye = new THREE.Vector3(),
_movePrev = new THREE.Vector2(),
_moveCurr = new THREE.Vector2(),
_lastAxis = new THREE.Vector3(),
_lastAngle = 0,
_zoomStart = new THREE.Vector2(),
_zoomEnd = new THREE.Vector2(),
_touchZoomDistanceStart = 0,
_touchZoomDistanceEnd = 0,
_panStart = new THREE.Vector2(),
_panEnd = new THREE.Vector2();
// for reset
this.target0 = this.target.clone();
this.position0 = this.object.position.clone();
this.up0 = this.object.up.clone();
// events
var changeEvent = { type: "change" };
var startEvent = { type: "start" };
var endEvent = { type: "end" };
// methods
this.handleResize = function () {
if (this.domElement === document) {
this.screen.left = 0;
this.screen.top = 0;
this.screen.width = window.innerWidth;
this.screen.height = window.innerHeight;
} else {
var box = this.domElement.getBoundingClientRect();
// adjustments come from similar code in the jquery offset() function
var d = this.domElement.ownerDocument.documentElement;
this.screen.left = box.left + window.pageXOffset - d.clientLeft;
this.screen.top = box.top + window.pageYOffset - d.clientTop;
this.screen.width = box.width;
this.screen.height = box.height;
}
};
var getMouseOnScreen = (function () {
var vector = new THREE.Vector2();
return function getMouseOnScreen(pageX, pageY) {
vector.set(
(pageX - _this.screen.left) / _this.screen.width,
(pageY - _this.screen.top) / _this.screen.height
);
return vector;
};
})();
var getMouseOnCircle = (function () {
var vector = new THREE.Vector2();
return function getMouseOnCircle(pageX, pageY) {
vector.set(
(pageX - _this.screen.width * 0.5 - _this.screen.left) /
(_this.screen.width * 0.5),
(_this.screen.height + 2 * (_this.screen.top - pageY)) /
_this.screen.width // screen.width intentional
);
return vector;
};
})();
this.rotateCamera = (function () {
var axis = new THREE.Vector3(),
quaternion = new THREE.Quaternion(),
eyeDirection = new THREE.Vector3(),
objectUpDirection = new THREE.Vector3(),
objectSidewaysDirection = new THREE.Vector3(),
moveDirection = new THREE.Vector3(),
angle;
return function rotateCamera() {
moveDirection.set(
_moveCurr.x - _movePrev.x,
_moveCurr.y - _movePrev.y,
0
);
angle = moveDirection.length();
if (angle) {
_eye.copy(_this.object.position).sub(_this.target);
eyeDirection.copy(_eye).normalize();
objectUpDirection.copy(_this.object.up).normalize();
objectSidewaysDirection
.crossVectors(objectUpDirection, eyeDirection)
.normalize();
objectUpDirection.setLength(_moveCurr.y - _movePrev.y);
objectSidewaysDirection.setLength(_moveCurr.x - _movePrev.x);
moveDirection.copy(objectUpDirection.add(objectSidewaysDirection));
axis.crossVectors(moveDirection, _eye).normalize();
angle *= _this.rotateSpeed;
quaternion.setFromAxisAngle(axis, angle);
_eye.applyQuaternion(quaternion);
_this.object.up.applyQuaternion(quaternion);
_lastAxis.copy(axis);
_lastAngle = angle;
} else if (!_this.staticMoving && _lastAngle) {
_lastAngle *= Math.sqrt(1.0 - _this.dynamicDampingFactor);
_eye.copy(_this.object.position).sub(_this.target);
quaternion.setFromAxisAngle(_lastAxis, _lastAngle);
_eye.applyQuaternion(quaternion);
_this.object.up.applyQuaternion(quaternion);
}
_movePrev.copy(_moveCurr);
};
})();
this.zoomCamera = function () {
var factor;
if (_state === STATE.TOUCH_ZOOM_PAN) {
factor = _touchZoomDistanceStart / _touchZoomDistanceEnd;
_touchZoomDistanceStart = _touchZoomDistanceEnd;
_eye.multiplyScalar(factor);
} else {
factor = 1.0 + (_zoomEnd.y - _zoomStart.y) * _this.zoomSpeed;
if (factor !== 1.0 && factor > 0.0) {
_eye.multiplyScalar(factor);
}
if (_this.staticMoving) {
_zoomStart.copy(_zoomEnd);
} else {
_zoomStart.y +=
(_zoomEnd.y - _zoomStart.y) * this.dynamicDampingFactor;
}
}
};
this.panCamera = (function () {
var mouseChange = new THREE.Vector2(),
objectUp = new THREE.Vector3(),
pan = new THREE.Vector3();
return function panCamera() {
mouseChange.copy(_panEnd).sub(_panStart);
if (mouseChange.lengthSq()) {
mouseChange.multiplyScalar(_eye.length() * _this.panSpeed);
pan.copy(_eye).cross(_this.object.up).setLength(mouseChange.x);
pan.add(objectUp.copy(_this.object.up).setLength(mouseChange.y));
_this.object.position.add(pan);
_this.target.add(pan);
if (_this.staticMoving) {
_panStart.copy(_panEnd);
} else {
_panStart.add(
mouseChange
.subVectors(_panEnd, _panStart)
.multiplyScalar(_this.dynamicDampingFactor)
);
}
}
};
})();
this.checkDistances = function () {
if (!_this.noZoom || !_this.noPan) {
if (_eye.lengthSq() > _this.maxDistance * _this.maxDistance) {
_this.object.position.addVectors(
_this.target,
_eye.setLength(_this.maxDistance)
);
_zoomStart.copy(_zoomEnd);
}
if (_eye.lengthSq() < _this.minDistance * _this.minDistance) {
_this.object.position.addVectors(
_this.target,
_eye.setLength(_this.minDistance)
);
_zoomStart.copy(_zoomEnd);
}
}
};
this.update = function () {
_eye.subVectors(_this.object.position, _this.target);
if (!_this.noRotate) {
_this.rotateCamera();
}
if (!_this.noZoom) {
_this.zoomCamera();
}
if (!_this.noPan) {
_this.panCamera();
}
_this.object.position.addVectors(_this.target, _eye);
_this.checkDistances();
_this.object.lookAt(_this.target);
if (lastPosition.distanceToSquared(_this.object.position) > EPS) {
_this.dispatchEvent(changeEvent);
lastPosition.copy(_this.object.position);
}
};
this.reset = function () {
_state = STATE.NONE;
_prevState = STATE.NONE;
_this.target.copy(_this.target0);
_this.object.position.copy(_this.position0);
_this.object.up.copy(_this.up0);
_eye.subVectors(_this.object.position, _this.target);
_this.object.lookAt(_this.target);
_this.dispatchEvent(changeEvent);
lastPosition.copy(_this.object.position);
};
// listeners
function keydown(event) {
if (_this.enabled === false) return;
window.removeEventListener("keydown", keydown);
_prevState = _state;
if (_state !== STATE.NONE) {
return;
} else if (
event.keyCode === _this.keys[STATE.ROTATE] &&
!_this.noRotate
) {
_state = STATE.ROTATE;
} else if (event.keyCode === _this.keys[STATE.ZOOM] && !_this.noZoom) {
_state = STATE.ZOOM;
} else if (event.keyCode === _this.keys[STATE.PAN] && !_this.noPan) {
_state = STATE.PAN;
}
}
function keyup(event) {
if (_this.enabled === false) return;
_state = _prevState;
window.addEventListener("keydown", keydown, false);
}
function mousedown(event) {
if (_this.enabled === false) return;
event.preventDefault();
event.stopPropagation();
if (_state === STATE.NONE) {
_state = event.button;
}
// 阻止浏览器的默认行为
return;
if (_state === STATE.ROTATE && !_this.noRotate) {
_moveCurr.copy(getMouseOnCircle(event.pageX, event.pageY));
_movePrev.copy(_moveCurr);
} else if (_state === STATE.ZOOM && !_this.noZoom) {
_zoomStart.copy(getMouseOnScreen(event.pageX, event.pageY));
_zoomEnd.copy(_zoomStart);
} else if (_state === STATE.PAN && !_this.noPan) {
_panStart.copy(getMouseOnScreen(event.pageX, event.pageY));
_panEnd.copy(_panStart);
}
document.addEventListener("mousemove", mousemove, false);
document.addEventListener("mouseup", mouseup, false);
_this.dispatchEvent(startEvent);
}
function mousemove(event) {
if (_this.enabled === false) return;
event.preventDefault();
event.stopPropagation();
if (_state === STATE.ROTATE && !_this.noRotate) {
_movePrev.copy(_moveCurr);
_moveCurr.copy(getMouseOnCircle(event.pageX, event.pageY));
} else if (_state === STATE.ZOOM && !_this.noZoom) {
_zoomEnd.copy(getMouseOnScreen(event.pageX, event.pageY));
} else if (_state === STATE.PAN && !_this.noPan) {
_panEnd.copy(getMouseOnScreen(event.pageX, event.pageY));
}
}
function mouseup(event) {
if (_this.enabled === false) return;
event.preventDefault();
event.stopPropagation();
_state = STATE.NONE;
document.removeEventListener("mousemove", mousemove);
document.removeEventListener("mouseup", mouseup);
_this.dispatchEvent(endEvent);
}
function mousewheel(event) {
if (_this.enabled === false) return;
if (_this.noZoom === true) return;
event.preventDefault();
event.stopPropagation();
// return;
switch (event.deltaMode) {
case 2:
// Zoom in pages
_zoomStart.y -= event.deltaY * 0.025;
break;
case 1:
// Zoom in lines
_zoomStart.y -= event.deltaY * 0.01;
break;
default:
// undefined, 0, assume pixels
_zoomStart.y -= event.deltaY * 0.00025;
break;
}
_this.dispatchEvent(startEvent);
_this.dispatchEvent(endEvent);
}
function touchstart(event) {
if (_this.enabled === false) return;
event.preventDefault();
switch (event.touches.length) {
case 1:
_state = STATE.TOUCH_ROTATE;
_moveCurr.copy(
getMouseOnCircle(event.touches[0].pageX, event.touches[0].pageY)
);
_movePrev.copy(_moveCurr);
break;
default: // 2 or more
_state = STATE.TOUCH_ZOOM_PAN;
var dx = event.touches[0].pageX - event.touches[1].pageX;
var dy = event.touches[0].pageY - event.touches[1].pageY;
_touchZoomDistanceEnd = _touchZoomDistanceStart = Math.sqrt(
dx * dx + dy * dy
);
var x = (event.touches[0].pageX + event.touches[1].pageX) / 2;
var y = (event.touches[0].pageY + event.touches[1].pageY) / 2;
_panStart.copy(getMouseOnScreen(x, y));
_panEnd.copy(_panStart);
break;
}
_this.dispatchEvent(startEvent);
}
function touchmove(event) {
if (_this.enabled === false) return;
event.preventDefault();
event.stopPropagation();
switch (event.touches.length) {
case 1:
_movePrev.copy(_moveCurr);
_moveCurr.copy(
getMouseOnCircle(event.touches[0].pageX, event.touches[0].pageY)
);
break;
default: // 2 or more
var dx = event.touches[0].pageX - event.touches[1].pageX;
var dy = event.touches[0].pageY - event.touches[1].pageY;
_touchZoomDistanceEnd = Math.sqrt(dx * dx + dy * dy);
var x = (event.touches[0].pageX + event.touches[1].pageX) / 2;
var y = (event.touches[0].pageY + event.touches[1].pageY) / 2;
_panEnd.copy(getMouseOnScreen(x, y));
break;
}
}
function touchend(event) {
if (_this.enabled === false) return;
switch (event.touches.length) {
case 0:
_state = STATE.NONE;
break;
case 1:
_state = STATE.TOUCH_ROTATE;
_moveCurr.copy(
getMouseOnCircle(event.touches[0].pageX, event.touches[0].pageY)
);
_movePrev.copy(_moveCurr);
break;
}
_this.dispatchEvent(endEvent);
}
function contextmenu(event) {
if (_this.enabled === false) return;
event.preventDefault();
}
this.dispose = function () {
this.domElement.removeEventListener("contextmenu", contextmenu, false);
this.domElement.removeEventListener("mousedown", mousedown, false);
// this.domElement.removeEventListener("wheel", mousewheel, false);
this.domElement.removeEventListener("touchstart", touchstart, false);
this.domElement.removeEventListener("touchend", touchend, false);
this.domElement.removeEventListener("touchmove", touchmove, false);
document.removeEventListener("mousemove", mousemove, false);
document.removeEventListener("mouseup", mouseup, false);
window.removeEventListener("keydown", keydown, false);
window.removeEventListener("keyup", keyup, false);
};
this.domElement.addEventListener("contextmenu", contextmenu, false);
this.domElement.addEventListener("mousedown", mousedown, false);
// this.domElement.addEventListener("wheel", mousewheel, false);
this.domElement.addEventListener("touchstart", touchstart, false);
this.domElement.addEventListener("touchend", touchend, false);
this.domElement.addEventListener("touchmove", touchmove, false);
window.addEventListener("keydown", keydown, false);
window.addEventListener("keyup", keyup, false);
this.handleResize();
// force an update at start
this.update();
}
}

55
src/utils/config.js

@ -0,0 +1,55 @@
const NUMBER_MATRIX = [
[
//H
[0, 0],
[3, 0],
[0, 1],
[3, 1],
[0, 2],
[1, 2],
[2, 2],
[3, 2],
[0, 3],
[3, 3],
[0, 4],
[3, 4],
],
[
//L
[0, 0],
[0, 1],
[0, 2],
[0, 3],
[0, 4],
[1, 4],
[2, 4],
[3, 4],
],
[
//2
[0, 0],
[1, 0],
[2, 0],
[3, 0],
[3, 1],
[2, 2],
[1, 3],
[0, 4],
[1, 4],
[2, 4],
[3, 4],
],
[
//7
[0, 0],
[1, 0],
[2, 0],
[3, 0],
[3, 1],
[2, 2],
[1, 3],
[1, 4],
],
];
export { NUMBER_MATRIX };

260
src/utils/prizeList.js

@ -0,0 +1,260 @@
import * as TWEEN from "./tween.min.js";
const MAX_TOP = 300;
const MAX_WIDTH = document.body.clientWidth;
let defaultType = 0;
let prizes;
let lastDanMuList = [];
let prizeElement = {};
let lasetPrizeIndex = 0;
const DEFAULT_MESS = [
"我是该抽中一等奖还是一等奖呢,纠结ing...",
"听说要提前一个月吃素才能中大奖喔!",
"好想要一等奖啊!!!",
"一等奖有没有人想要呢?",
"五等奖也不错,只要自己能中奖就行",
"祝大家新年快乐!",
"中不中奖不重要,大家吃好喝好。",
"新年,祝福大家事事顺遂。",
"作为专业陪跑的我,我就看看你们有谁跟我一样",
"新的一年祝福大家越来越好!",
"来年再战!!!",
];
class DanMu {
constructor(option) {
if (typeof option !== "object") {
option = {
text: option,
};
}
this.position = {};
this.text = option.text;
this.onComplete = option.onComplete;
this.init();
}
init() {
this.element = document.createElement("div");
this.element.className = "dan-mu";
document.body.appendChild(this.element);
this.start();
}
setText(text) {
this.text = text || this.text;
this.element.textContent = this.text;
this.width = this.element.clientWidth + 100;
}
start(text) {
let speed = ~~(Math.random() * 10000) + 6000;
this.position = {
x: MAX_WIDTH,
};
let delay = speed / 10;
this.setText(text);
this.element.style.transform = "translateX(" + this.position.x + "px)";
this.element.style.top = ~~(Math.random() * MAX_TOP) + 10 + "px";
this.element.classList.add("active");
this.tween = new TWEEN.Tween(this.position)
.to(
{
x: -this.width,
},
speed
)
.onUpdate(() => {
this.render();
})
.onComplete(() => {
this.onComplete && this.onComplete();
})
.start();
}
render() {
this.element.style.transform = "translateX(" + this.position.x + "px)";
}
}
class Qipao {
constructor(option) {
if (typeof option !== "object") {
option = {
text: option,
};
}
this.text = option.text;
this.onComplete = option.onComplete;
this.$par = document.querySelector(".qipao-container");
if (!this.$par) {
this.$par = document.createElement("div");
this.$par.className = "qipao-container";
document.body.appendChild(this.$par);
}
this.init();
}
init() {
this.element = document.createElement("div");
this.element.className = "qipao animated";
this.$par.appendChild(this.element);
this.start();
}
setText(text) {
this.text = text || this.text;
this.element.textContent = this.text;
}
start(text) {
this.setText(text);
this.element.classList.remove("bounceOutRight");
this.element.classList.add("bounceInRight");
setTimeout(() => {
this.element.classList.remove("bounceInRight");
this.element.classList.add("bounceOutRight");
this.onComplete && this.onComplete();
}, 4000);
}
}
function setPrizes(pri) {
prizes = pri;
defaultType = prizes[0]["type"];
lasetPrizeIndex = pri.length - 1;
}
function showPrizeList(currentPrizeIndex) {
let currentPrize = prizes[currentPrizeIndex];
if (currentPrize.type === defaultType) {
currentPrize.count = "不限制";
}
}
function resetPrize(currentPrizeIndex) {
prizeElement = {};
lasetPrizeIndex = currentPrizeIndex;
showPrizeList(currentPrizeIndex);
}
let setPrizeData = (function () {
return function (currentPrizeIndex, count, isInit) {
let currentPrize = prizes[currentPrizeIndex],
type = currentPrize.type,
elements = prizeElement[type],
totalCount = currentPrize.count;
if (!elements) {
elements = {
box: document.querySelector(`#prize-item-${type}`),
bar: document.querySelector(`#prize-bar-${type}`),
text: document.querySelector(`#prize-count-${type}`),
};
prizeElement[type] = elements;
}
if (!prizeElement.prizeType) {
prizeElement.prizeType = document.querySelector("#prizeType");
prizeElement.prizeLeft = document.querySelector("#prizeLeft");
prizeElement.prizeText = document.querySelector("#prizeText");
}
if (isInit) {
for (let i = prizes.length - 1; i > currentPrizeIndex; i--) {
let type = prizes[i]["type"];
document.querySelector(`#prize-item-${type}`).className =
"prize-item done";
document.querySelector(`#prize-bar-${type}`).style.width = "0";
document.querySelector(`#prize-count-${type}`).textContent =
"0" + "/" + prizes[i]["count"];
}
}
if (lasetPrizeIndex !== currentPrizeIndex) {
let lastPrize = prizes[lasetPrizeIndex],
lastBox = document.querySelector(`#prize-item-${lastPrize.type}`);
lastBox.classList.remove("shine");
lastBox.classList.add("done");
elements.box && elements.box.classList.add("shine");
prizeElement.prizeType.textContent = currentPrize.text;
prizeElement.prizeText.textContent = currentPrize.title;
lasetPrizeIndex = currentPrizeIndex;
}
if (currentPrizeIndex === 0) {
prizeElement.prizeType.textContent = "特别奖";
prizeElement.prizeText.textContent = " ";
prizeElement.prizeLeft.textContent = "不限制";
return;
}
count = totalCount - count;
count = count < 0 ? 0 : count;
let percent = (count / totalCount).toFixed(2);
elements.bar && (elements.bar.style.width = percent * 100 + "%");
elements.text && (elements.text.textContent = count + "/" + totalCount);
prizeElement.prizeLeft.textContent = count;
};
})();
function startMaoPao() {
let len = DEFAULT_MESS.length,
count = 5,
index = ~~(Math.random() * len),
danmuList = [],
total = 0;
function restart() {
total = 0;
danmuList.forEach((item) => {
let text =
lastDanMuList.length > 0
? lastDanMuList.shift()
: DEFAULT_MESS[index++];
item.start(text);
index = index > len ? 0 : index;
});
}
for (let i = 0; i < count; i++) {
setTimeout(() => {
danmuList.push(
new DanMu({
text: DEFAULT_MESS[index++],
onComplete: function () {
setTimeout(() => {
this.start(DEFAULT_MESS[index++]);
index = index > len ? 0 : index;
}, 1000);
},
})
);
index = index > len ? 0 : index;
}, 1500 * i);
}
}
function addDanMu(text) {
lastDanMuList.push(text);
}
export {
startMaoPao,
showPrizeList,
setPrizeData,
addDanMu,
setPrizes,
resetPrize,
};

113
src/utils/request.js

@ -1,25 +1,106 @@
import axios from 'axios'
import { ElMessage } from 'element-plus'
// 创建基础实例
const ERROR_MESSAGES = {
badRequest: '请求错误(400)',
unauthorized: '未授权,请登录(401)',
forbidden: '拒绝访问(403)',
notFound: `请求地址出错: ${'[具体 URL 将在这里被替换]'}`,
methodNotAllowed: '请求方法未允许(405)',
requestTimeout: '请求超时(408)',
internalServerError: '服务器内部错误(500)',
notImplemented: '服务未实现(501)',
badGateway: '网络错误(502)',
serviceUnavailable: '服务不可用(503)',
gatewayTimeout: '网络超时(504)',
httpVersionNotSupported: 'HTTP 版本不受支持(505)',
defaultConnectionError: '连接错误: [原始错误消息]',
networkError: '网络异常,请检查后重试!',
serverFailure: '连接到服务器失败,请联系管理员'
}
const service = axios.create({
baseURL: import.meta.env.VITE_APP_BASE_API,
timeout: 10000
baseURL: '/api', // 使用代理路径作为baseURL
// timeout: 50000,
withCredentials: false // send cookies when cross-domain requests
// headers: {
// // clear cors
// 'Cache-Control': 'no-cache',
// Pragma: 'no-cache'
// }
})
// 请求拦截器(简化版)
service.interceptors.request.use(config => {
// 添加token逻辑(如果存在)
const token = localStorage.getItem('token')
if (token) {
config.headers['Authorization'] = 'Bearer ' + token
}
// 添加固定的 token
config.headers['token'] = '+SsksARQgUHIbIG3rRnnbZi0+fEeMx8pywnIlrmTxo5EOPR/wjWDV7w7+ZUseiBtf9kFa/atmNx6QfSpv5w'
return config
})
const setErrorMsg = (error) => {
if (error && error.response) {
switch (error.response.status) {
case 400:
error.message = ERROR_MESSAGES.badRequest
break
case 401:
error.message = ERROR_MESSAGES.unauthorized
break
case 403:
error.message = ERROR_MESSAGES.forbidden
break
case 404:
error.message = ERROR_MESSAGES.notFound.replace(
'[具体 URL 将在这里被替换]',
error.response.config.url
)
break
case 405:
error.message = ERROR_MESSAGES.methodNotAllowed
break
case 408:
error.message = ERROR_MESSAGES.requestTimeout
break
case 500:
error.message = ERROR_MESSAGES.internalServerError
break
case 501:
error.message = ERROR_MESSAGES.notImplemented
break
case 502:
error.message = ERROR_MESSAGES.badGateway
break
case 503:
error.message = ERROR_MESSAGES.serviceUnavailable
break
case 504:
error.message = ERROR_MESSAGES.gatewayTimeout
break
case 505:
error.message = ERROR_MESSAGES.httpVersionNotSupported
break
default:
error.message = ERROR_MESSAGES.defaultConnectionError.replace(
'[原始错误消息]',
error.message
)
}
} else {
if (error.message === 'Network Error') {
error.message = ERROR_MESSAGES.networkError
} else {
error.message = ERROR_MESSAGES.serverFailure
}
}
return error.message
}
// Request interceptors
service.interceptors.request.use(
(config) => {
// 在此处添加请求头等,如添加 token
// if (store.state.token) {
// config.headers['Authorization'] = `Bearer ${store.state.token}`
// }
return config
},
(error) => {
return Promise.reject(error)
}
)
// 响应拦截器(简化版)
service.interceptors.response.use(

957
src/utils/three.min.js
File diff suppressed because it is too large
View File

1
src/utils/tween.min.js

@ -0,0 +1 @@
"use strict";var TWEEN=TWEEN||(function(){var b=[];return{REVISION:"7",getAll:function(){return b},removeAll:function(){b=[]},add:function(a){b.push(a)},remove:function(a){a=b.indexOf(a);-1!==a&&b.splice(a,1)},update:function(f){if(0===b.length){return !1}for(var a=0,e=b.length,f=void 0!==f?f:Date.now();a<e;){b[a].update(f)?a++:(b.splice(a,1),e--)}return !0}}})();TWEEN.Tween=function(A){var y={},z={},x=1000,w=0,v=null,u=TWEEN.Easing.Linear.None,g=TWEEN.Interpolation.Linear,t=[],q=null,o=!1,j=null,B=null,i=null;this.to=function(b,d){null!==d&&(x=d);z=b;return this};this.start=function(b){TWEEN.add(this);o=!1;v=void 0!==b?b:Date.now();v+=w;for(var a in z){if(null!==A[a]){if(z[a] instanceof Array){if(0===z[a].length){continue}z[a]=[A[a]].concat(z[a])}y[a]=A[a]}}return this};this.stop=function(){TWEEN.remove(this);B&&B.call(this);return this};this.delay=function(b){w=b;return this};this.easing=function(b){u=b;return this};this.interpolation=function(b){g=b;return this};this.chain=function(){t=arguments;return this};this.onStart=function(b){q=b;return this};this.onStop=function(b){B=b;return this};this.onUpdate=function(b){j=b;return this};this.onComplete=function(b){i=b;return this};this.update=function(h){if(h<v){return !0}!1===o&&(null!==q&&q.call(A),(o=!0));var d=(h-v)/x,d=1<d?1:d,b=u(d),a;for(a in y){var c=y[a],f=z[a];A[a]=f instanceof Array?g(f,b):c+(f-c)*b}null!==j&&j.call(A,b);if(1==d){null!==i&&i.call(A);d=0;for(b=t.length;d<b;d++){t[d].start(h)}return !1}return !0}};TWEEN.Easing={Linear:{None:function(b){return b}},Quadratic:{In:function(b){return b*b},Out:function(b){return b*(2-b)},InOut:function(b){return 1>(b*=2)?0.5*b*b:-0.5*(--b*(b-2)-1)}},Cubic:{In:function(b){return b*b*b},Out:function(b){return --b*b*b+1},InOut:function(b){return 1>(b*=2)?0.5*b*b*b:0.5*((b-=2)*b*b+2)}},Quartic:{In:function(b){return b*b*b*b},Out:function(b){return 1- --b*b*b*b},InOut:function(b){return 1>(b*=2)?0.5*b*b*b*b:-0.5*((b-=2)*b*b*b-2)}},Quintic:{In:function(b){return b*b*b*b*b},Out:function(b){return --b*b*b*b*b+1},InOut:function(b){return 1>(b*=2)?0.5*b*b*b*b*b:0.5*((b-=2)*b*b*b*b+2)}},Sinusoidal:{In:function(b){return 1-Math.cos((b*Math.PI)/2)},Out:function(b){return Math.sin((b*Math.PI)/2)},InOut:function(b){return 0.5*(1-Math.cos(Math.PI*b))}},Exponential:{In:function(b){return 0===b?0:Math.pow(1024,b-1)},Out:function(b){return 1===b?1:1-Math.pow(2,-10*b)},InOut:function(b){return 0===b?0:1===b?1:1>(b*=2)?0.5*Math.pow(1024,b-1):0.5*(-Math.pow(2,-10*(b-1))+2)}},Circular:{In:function(b){return 1-Math.sqrt(1-b*b)},Out:function(b){return Math.sqrt(1- --b*b)},InOut:function(b){return 1>(b*=2)?-0.5*(Math.sqrt(1-b*b)-1):0.5*(Math.sqrt(1-(b-=2)*b)+1)}},Elastic:{In:function(e){var f,d=0.1;if(0===e){return 0}if(1===e){return 1}!d||1>d?((d=1),(f=0.1)):(f=(0.4*Math.asin(1/d))/(2*Math.PI));return -(d*Math.pow(2,10*(e-=1))*Math.sin(((e-f)*2*Math.PI)/0.4))},Out:function(e){var f,d=0.1;if(0===e){return 0}if(1===e){return 1}!d||1>d?((d=1),(f=0.1)):(f=(0.4*Math.asin(1/d))/(2*Math.PI));return(d*Math.pow(2,-10*e)*Math.sin(((e-f)*2*Math.PI)/0.4)+1)},InOut:function(e){var f,d=0.1;if(0===e){return 0}if(1===e){return 1}!d||1>d?((d=1),(f=0.1)):(f=(0.4*Math.asin(1/d))/(2*Math.PI));return 1>(e*=2)?-0.5*d*Math.pow(2,10*(e-=1))*Math.sin(((e-f)*2*Math.PI)/0.4):0.5*d*Math.pow(2,-10*(e-=1))*Math.sin(((e-f)*2*Math.PI)/0.4)+1}},Back:{In:function(b){return b*b*(2.70158*b-1.70158)},Out:function(b){return --b*b*(2.70158*b+1.70158)+1},InOut:function(b){return 1>(b*=2)?0.5*b*b*(3.5949095*b-2.5949095):0.5*((b-=2)*b*(3.5949095*b+2.5949095)+2)}},Bounce:{In:function(b){return 1-TWEEN.Easing.Bounce.Out(1-b)},Out:function(b){return b<1/2.75?7.5625*b*b:b<2/2.75?7.5625*(b-=1.5/2.75)*b+0.75:b<2.5/2.75?7.5625*(b-=2.25/2.75)*b+0.9375:7.5625*(b-=2.625/2.75)*b+0.984375},InOut:function(b){return 0.5>b?0.5*TWEEN.Easing.Bounce.In(2*b):0.5*TWEEN.Easing.Bounce.Out(2*b-1)+0.5}}};TWEEN.Interpolation={Linear:function(h,l){var g=h.length-1,k=g*l,j=Math.floor(k),i=TWEEN.Interpolation.Utils.Linear;return 0>l?i(h[0],h[1],k):1<l?i(h[g],h[g-1],g-k):i(h[j],h[j+1>g?g:j+1],k-j)},Bezier:function(i,n){var g=0,m=i.length-1,l=Math.pow,k=TWEEN.Interpolation.Utils.Bernstein,j;for(j=0;j<=m;j++){g+=l(1-n,m-j)*l(n,j)*i[j]*k(m,j)}return g},CatmullRom:function(h,l){var g=h.length-1,k=g*l,j=Math.floor(k),i=TWEEN.Interpolation.Utils.CatmullRom;return h[0]===h[g]?(0>l&&(j=Math.floor((k=g*(1+l)))),i(h[(j-1+g)%g],h[j],h[(j+1)%g],h[(j+2)%g],k-j)):0>l?h[0]-(i(h[0],h[0],h[1],h[1],-k)-h[0]):1<l?h[g]-(i(h[g],h[g],h[g-1],h[g-1],k-g)-h[g]):i(h[j?j-1:0],h[j],h[g<j+1?g:j+1],h[g<j+2?g:j+2],k-j)},Utils:{Linear:function(e,f,d){return(f-e)*d+e},Bernstein:function(e,f){var d=TWEEN.Interpolation.Utils.Factorial;return d(e)/d(f)/d(e-f)},Factorial:(function(){var b=[1];return function(f){var a=1,e;if(b[f]){return b[f]}for(e=f;1<e;e--){a*=e}return(b[f]=a)}})(),CatmullRom:function(h,l,g,k,j){var h=0.5*(g-h),k=0.5*(k-l),i=j*j;return((2*l-2*g+h+k)*j*i+(-3*l+3*g-2*h-k)*i+h*j+l)}}};

101
src/views/choujiang/Login.vue

@ -0,0 +1,101 @@
<template>
<div class="login-container">
<div class="login-card">
<form @submit.prevent="handleLogin">
<div class="form-group">
<input
type="password"
id="password"
v-model="password"
placeholder="请输入密码"
/>
</div>
<button type="submit" class="login-button">进入抽奖</button>
</form>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue';
import { useRouter } from 'vue-router';
import { useAuthStore } from '../../stores/auth';
const password = ref('');
const CORRECT_PASSWORD = '123456';
const router = useRouter();
const authStore = useAuthStore();
const handleLogin = () => {
if (password.value === '') {
alert('请输入密码');
return;
}
if (password.value === CORRECT_PASSWORD) {
//
authStore.login(); // 使Pinia
router.push('/choujiang');
} else {
alert('密码错误,请重试');
}
};
//
console.log('登录信息:', {
password: password.value,
});
// API
</script>
<style scoped>
.login-container {
background-image: url('../../assets/登录背景.png'); /* 确保路径正确 */
background-position: center;
background-size: cover;
height: 100vh; /* 确保背景图片覆盖整个视口高度 */
width: 100vw; /* 确保背景图片覆盖整个视口宽度 */
position: fixed; /* 使用fixed定位确保背景图片覆盖整个页面 */
top: 0;
left: 0;
/* z-index: -1; 确保背景图片在其他内容下方 */
}
.login-card {
position: absolute;
top: 55%;
left: 50%;
transform: translate(-50%, -50%);
width: 450px; /* 增加卡片宽度 */
height: 285px;
padding: 2.5rem; /* 增加内边距 */
background: rgba(255, 255, 255, 0.3); /* 调整背景颜色为半透明白色 */
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
box-sizing: border-box;
z-index: 1; /* 确保卡片在背景图片上方 */
}
input {
width: 100%;
padding: 1.2rem;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 1.3rem; /* 增大字体大小 */
margin-bottom: 2.5rem; /* 增加底部间距 */
box-sizing: border-box
}
.login-button {
width: 100%;
padding: 1.2rem; /* 增加按钮内边距 */
background-color: #e92821a3;
color: white;
border: none;
border-radius: 4px;
font-size: 1.3rem; /* 增大字体大小 */
cursor: pointer;
transition: background-color 0.3s;
box-sizing: border-box
}
.login-button:hover {
transform: scale(1.03);
}
</style>

2153
src/views/choujiang/hxl-cj/cj.vue
File diff suppressed because it is too large
View File

347
src/views/choujiang/index.vue

@ -1,13 +1,356 @@
<template>
<div>
抽奖
<div class="choujiang-main">
<Lottery3D ref="lottery3DRef" />
<PrizePanel :prizes="dataManager.state.basicData.prizes" />
<ControlBar
:lottery-state="lotteryState"
:is-disabled="isDisabled"
@lottery-click="handleLotteryClick"
@reset="handleReset"
@export="handleExport"
/>
<MusicPlayer ref="musicPlayerRef" />
<Mascot />
<!-- 透明弹窗 -->
<div v-if="showPrizeExhaustedModal" class="prize-exhausted-modal">
<div class="modal-content">
<p class="modal-text">该礼品已抽取完毕请揭秘下一个礼品</p>
</div>
</div>
<div v-if="showPrizeExhaustedModal1" class="prize-exhausted-modal">
<div class="modal-content">
<p class="modal-text">请先揭秘一个礼品</p>
</div>
</div>
<!-- <UserList
:lucky-users="
dataManager.state.basicData.luckyUsers[
dataManager.state.currentPrize?.type
] || []
"
:left-users="dataManager.state.basicData.leftUsers"
/> -->
<!-- <Qipao :text="qipaoText" :show="showQipao" /> -->
</div>
</template>
<script setup>
import Lottery3D from "./lottery/Lottery3D.vue";
import PrizePanel from "./lottery/PrizePanel.vue";
import ControlBar from "./lottery/ControlBar.vue";
import MusicPlayer from "./lottery/MusicPlayer.vue";
import Qipao from "./lottery/Qipao.vue";
import UserList from "./lottery/UserList.vue";
import Mascot from "./lottery/Mascot.vue";
import { ref, onMounted, nextTick, computed, watch } from "vue";
import { useDataManager } from "./lottery/dataManager.js";
import { useLotteryEngine } from "./lottery/lotteryEngine.js";
import { useLotteryStore } from "../../store/lottery"; //
import { drawLottery } from "../../api/API";
const qipaoText = ref("");
const showQipao = ref(false);
const showPrizeExhaustedModal = ref(false);
const showPrizeExhaustedModal1 = ref(false);
// const lotteryState = ref('idle'); // idle, ready, rotating, result
//
const lotteryStore = useLotteryStore();
const lotteryState = computed({
get: () => lotteryStore.lotteryState,
set: (val) => lotteryStore.setLotteryState(val),
});
const lastRevealed = computed({
get: () => lotteryStore.lastRevealedIdx,
set: (val) => lotteryStore.setLastRevealedIdx(val),
});
const waitingForNextReveal = computed({
get: () => lotteryStore.waitingForNextReveal,
set: (val) => lotteryStore.setWaitingForNextReveal(val),
});
const winnerList = computed({
get: () => lotteryStore.winnerList,
set: (val) => lotteryStore.setWinnerList(val),
});
const isDisabled = ref(false);
watch(isDisabled, (newVal, oldVal) => {
console.log("isDisabled 变化:", oldVal, "->", newVal);
});
//
const dataManager = useDataManager();
let lottery3DRef = ref(null);
let musicPlayerRef = ref(null);
const lotteryEngine = useLotteryEngine(dataManager, {
resetCard: (...args) => lottery3DRef.value?.resetCard?.(...args),
addHighlight: (...args) => lottery3DRef.value?.addHighlight?.(...args),
switchScreen: (...args) => lottery3DRef.value?.switchScreen?.(...args),
rotateBallStart: (...args) => lottery3DRef.value?.rotateBallStart?.(...args),
rotateBallStop: (...args) => lottery3DRef.value?.rotateBallStop?.(...args),
selectCard: (...args) => lottery3DRef.value?.selectCard?.(...args),
});
onMounted(async () => {
isDisabled.value = true;
setTimeout(() => {
isDisabled.value = false;
}, 4000);
await dataManager.getBasicData();
await dataManager.getUsers();
// dataManager window 使
window.dataManager = dataManager;
//
setTimeout(() => {
if (musicPlayerRef.value && !musicPlayerRef.value.isPlaying()) {
//
musicPlayerRef.value.toggleMusic();
}
}, 1000);
});
function showLotteryQipao() {
const luckys = dataManager.state.currentLuckys;
const prize = dataManager.state.currentPrize;
if (!luckys || luckys.length === 0) return;
// jwcode username
const names = luckys
.map((item) => item.username || item[1] || item.jwcode || "")
.join("、");
qipaoText.value = `恭喜${names}获得${prize?.title || ""}`;
showQipao.value = true;
setTimeout(() => {
showQipao.value = false;
}, 3000);
}
async function handleLotteryClick() {
if (isDisabled.value) return; // 2
isDisabled.value = true;
// setTimeout(() => {
// isDisabled.value = false;
// }, 2000);
switch (lotteryState.value) {
case "idle":
setTimeout(() => {
isDisabled.value = false;
}, 2000);
//
await lottery3DRef.value?.switchScreen?.("lottery");
console.log("lotteryState 变更前:", lotteryState.value, "-> ready");
// await new Promise((resolve) => setTimeout(resolve, 2000));
lotteryState.value = "ready";
console.log("lotteryState 变更后:", lotteryState.value);
break;
case "ready":
if (waitingForNextReveal.value) {
console.log("waitingForNextReveal.value", waitingForNextReveal.value);
//
showPrizeExhaustedModal.value = true;
setTimeout(() => {
showPrizeExhaustedModal.value = false;
}, 1000);
isDisabled.value = false;
break;
}
if (lastRevealed.value === -1) {
console.log("lastRevealed.value", lastRevealed.value);
showPrizeExhaustedModal1.value = true;
setTimeout(() => {
showPrizeExhaustedModal1.value = false;
}, 1000);
isDisabled.value = false;
break;
}
console.log("lotteryState 变更前:", lotteryState.value, "-> rotating");
lotteryState.value = "rotating";
console.log("lotteryState 变更后:", lotteryState.value);
const prize = dataManager.state.basicData.prizes[lastRevealed.value];
console.log("准备调用 drawLottery,prize:", prize);
console.log("lastRevealed.value:", lastRevealed.value);
//
const rotatePromise = lottery3DRef.value?.rotateBallStart?.();
//
try {
winnerList.value = await drawLottery({
perWin: prize.perWin,
remainNum: prize.remainNum,
gradeId: prize.gradeId,
prizeId: prize.prizeId,
});
setTimeout(() => {
isDisabled.value = false;
}, 2000);
console.log("drawLottery 调用成功,结果:", winnerList.value);
} catch (error) {
console.error("drawLottery 调用失败:", error);
}
// await rotatePromise
break;
case "rotating":
setTimeout(() => {
isDisabled.value = false;
}, 2000);
//
// const prize = dataManager.state.basicData.prizes[lastRevealed.value];
// console.log(" drawLotteryprize:", prize);
// console.log("lastRevealed.value:", lastRevealed.value);
// try {
// winnerList.value = await drawLottery({
// perWin: prize.perWin,
// remainNum: prize.remainNum,
// gradeId: prize.gradeId,
// prizeId: prize.prizeId,
// });
// console.log("drawLottery :", winnerList.value);
// } catch (error) {
// console.error("drawLottery :", error);
// }
await lottery3DRef.value?.rotateBallStop?.();
await lotteryEngine.executeLottery();
await nextTick();
showLotteryQipao();
console.log("lotteryState 变更前:", lotteryState.value, "-> idle");
lotteryState.value = "result";
console.log("lotteryState 变更后:", lotteryState.value);
break;
case "result":
setTimeout(() => {
isDisabled.value = false;
}, 2800);
// result
await lottery3DRef.value?.switchScreen?.("lottery");
await new Promise((resolve) => setTimeout(resolve, 2500));
//
lottery3DRef.value?.changeCard1?.();
//2
lotteryState.value = "ready";
break;
default:
break;
}
}
function handleReset() {
lotteryEngine.resetLottery();
}
function handleExport() {
dataManager.exportData();
}
function handlePrevPrize() {
if (dataManager.state.currentPrizeIndex > 0) {
dataManager.state.currentPrizeIndex--;
dataManager.state.currentPrize =
dataManager.state.basicData.prizes[dataManager.state.currentPrizeIndex];
}
}
function handleNextPrize() {
if (
dataManager.state.currentPrizeIndex <
dataManager.state.basicData.prizes.length - 1
) {
dataManager.state.currentPrizeIndex++;
dataManager.state.currentPrize =
dataManager.state.basicData.prizes[dataManager.state.currentPrizeIndex];
}
}
</script>
<style scoped>
.choujiang-main {
width: 100vw;
height: 100vh;
position: fixed;
top: 0;
left: 0;
overflow: hidden;
/* 添加背景图片 */
background: url("../../assets/bg@2x.png");
background-size: 1920px 980px;
}
/* 透明弹窗样式 */
.prize-exhausted-modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: flex-start;
padding-top: 20vh;
z-index: 9999;
pointer-events: none;
}
.modal-content {
background: transparent;
padding: 20px 30px;
border-radius: 10px;
animation: fadeInOut 1s ease-in-out;
}
.modal-text {
color: #ff0000;
font-size: 18px;
font-weight: bold;
text-align: center;
margin: 0;
white-space: nowrap;
}
@keyframes fadeInOut {
0% {
opacity: 0;
transform: translateY(-20px);
}
20% {
opacity: 1;
transform: translateY(0);
}
80% {
opacity: 1;
transform: translateY(0);
}
100% {
opacity: 0;
transform: translateY(-20px);
}
}
</style>

185
src/views/choujiang/lottery/CardItem.vue

@ -0,0 +1,185 @@
<template>
<div
:id="`card-${id}`"
:class="[
'element',
{ lightitem: isBold, highlight: highlight, prize: prize },
]"
:style="cardStyle"
>
<!-- <div class="company">{{ company }}</div> -->
<!-- <div class="name">{{ user[1] }}</div> -->
<div class="details">{{ displayText }}</div>
</div>
</template>
<script setup>
import { computed, ref, onMounted, onBeforeUnmount, watch } from "vue";
import { useLotteryStore } from "../../../store/lottery"; //
const lotteryStore = useLotteryStore(); //
const lotteryState = computed({
get: () => lotteryStore.lotteryState,
set: (val) => lotteryStore.setLotteryState(val),
});
const props = defineProps({
id: [String, Number],
user: { type: Array, required: true },
isBold: Boolean,
showTable: Boolean,
company: String,
highlight: Boolean,
prize: Boolean,
});
//
const displayText = ref("");
const textSwitchInterval = ref(null);
// store
const allUsers = computed(() => lotteryStore.allUsers);
const isUsersLoaded = computed(() => lotteryStore.isUsersLoaded);
//
const switchText = () => {
if (isUsersLoaded.value && allUsers.value.length > 0) {
displayText.value = lotteryStore.getRandomUserName();
} else {
displayText.value = props.user[0] || "";
}
};
//
const startTextSwitch = () => {
//
if (lotteryState.value === "idle" || lotteryState.value === "ready") {
const scheduleNextSwitch = () => {
// 1-4
const randomInterval = Math.random() * 10000 + 10000; // 10000-20000
textSwitchInterval.value = setTimeout(() => {
switchText();
//
scheduleNextSwitch();
}, randomInterval);
};
scheduleNextSwitch();
}
};
//
const stopTextSwitch = () => {
if (textSwitchInterval.value) {
clearTimeout(textSwitchInterval.value);
textSwitchInterval.value = null;
}
};
//
const handleLotteryStateChange = () => {
if (lotteryState.value === "rotating" || lotteryState.value === "result") {
//
stopTextSwitch();
displayText.value = props.user[0] || "";
} else {
//
startTextSwitch();
}
};
const cardStyle = computed(() => {
//
const baseStyle = {
width: "130px",
height: "170px",
border: "1px solid rgb(255,255,255)",
};
if (props.isBold && props.showTable) {
if (lotteryState.value === "idle") {
return {
...baseStyle,
background: 'linear-gradient(180deg, rgba(243,153,38,0.7) 0%, rgba(207,56,35,1) 100%)',
};
}
}
// resultprizeCSS
if (lotteryState.value === "result" && props.prize) {
return baseStyle;
}
//
return {
...baseStyle,
backgroundColor: "rgba(254, 177, 48, 100)",
};
});
//
onMounted(async () => {
//
displayText.value = props.user[0] || "";
//
setTimeout(() => {
startTextSwitch();
}, 1000);
//
handleLotteryStateChange();
});
//
watch(lotteryState, () => {
handleLotteryStateChange();
});
//
watch(isUsersLoaded, (loaded) => {
if (loaded) {
//
switchText();
}
});
//
onBeforeUnmount(() => {
stopTextSwitch();
});
</script>
<style scoped>
.element {
transition: background 2s;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.4);
/* 你的基础样式 */
}
.lightitem {
/* 高亮样式 */
}
.highlight {
/* 响应式高亮样式 */
}
.prize {
/* 中奖样式 - 使用更高优先级 */
background: linear-gradient(180deg, #F39B26 0%, #E13A26 100%) !important;
}
.company {
/* ... */
}
.name {
/* ... */
}
.details {
font-size: 26px;
color: white;
text-align: center;
display: flex;
flex-direction: column;
justify-content: center;
height: 100%;
transition: opacity 0.3s ease;
}
</style>

78
src/views/choujiang/lottery/ControlBar.vue

@ -0,0 +1,78 @@
<template>
<div class="control-bar">
<button
:disabled="isDisabled"
@click="$emit('lottery-click')"
class="lottery-button"
>
{{ lotteryState === 'idle' ? '进入抽奖' : lotteryState === 'ready' ? '开始抽奖' : lotteryState === 'rotating' ? '结束抽奖' : '开始抽奖' }}
</button>
<!-- <button @click="$emit('reset')">重置</button> -->
<!-- <button @click="$emit('export')">导出结果</button> -->
</div>
</template>
<script setup>
const props = defineProps({
lotteryState: String,
isDisabled: Boolean
});
defineEmits(['lottery-click', 'reset', 'export']);
</script>
<style scoped>
.control-bar {
position: absolute;
bottom: 40px;
left: 50%;
transform: translateX(-50%);
display: flex;
gap: 24px;
z-index: 10;
}
.lottery-button {
padding: 12px 32px;
font-size: 20px;
font-weight: 700;
border-radius: 50px;
border: none;
background: linear-gradient(90deg, #ff8c42 0%, #ff6b35 50%, #ff5722 100%);
color: #fff;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 4px 15px rgba(2, 0, 0, 0.5);
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
min-width: 160px;
height: 50px;
display: flex;
align-items: center;
justify-content: center;
font-family: 'Microsoft YaHei', 'PingFang SC', sans-serif;
}
.lottery-button:hover {
background: linear-gradient(90deg, #ff9a5a 0%, #ff7a4a 50%, #ff6b3a 100%);
box-shadow: 0 6px 20px rgba(255, 87, 34, 0.4);
transform: translateY(-2px);
}
.lottery-button:active {
transform: translateY(0);
box-shadow: 0 2px 10px rgba(255, 87, 34, 0.3);
}
.lottery-button:disabled {
background: linear-gradient(90deg, #ccc 0%, #bbb 50%, #aaa 100%);
cursor: not-allowed;
box-shadow: none;
transform: none;
opacity: 0.6;
}
.lottery-button:disabled:hover {
background: linear-gradient(90deg, #ccc 0%, #bbb 50%, #aaa 100%);
box-shadow: none;
transform: none;
}
</style>

886
src/views/choujiang/lottery/Lottery3D.vue

@ -0,0 +1,886 @@
<template>
<div class="lottery-3d-container">
<div ref="threeContainer" class="three-container"></div>
<!-- 分页指示器 -->
<div v-if="totalPages > 1" class="page-indicator">
{{ currentPage + 1 }} / {{ totalPages }}
</div>
<!-- 滚动提示 -->
<!-- <div v-if="totalPages > 1 && currentPage === 0" class="scroll-hint">
向下滚动查看更多
</div>
<div v-if="totalPages > 1 && currentPage === totalPages - 1" class="scroll-hint">
向上滚动查看上一页
</div> -->
</div>
</template>
<script setup>
import {
ref,
onMounted,
onBeforeUnmount,
defineExpose,
watch,
computed,
} from "vue";
import * as THREE from "three";
import {
CSS3DRenderer,
CSS3DObject,
} from "three/examples/jsm/renderers/CSS3DRenderer.js";
// import { TrackballControls } from 'three/examples/jsm/controls/TrackballControls.js'; //
import TWEEN from "@tweenjs/tween.js";
import { NUMBER_MATRIX } from "../../../utils/config.js";
import CardItem from "./CardItem.vue";
import { createApp } from "vue";
import { getUserListApi } from "../../../api/API";
import { useLotteryStore } from "../../../store/lottery";
const lotteryStore = useLotteryStore();
const winners = computed({
get: () => lotteryStore.winners,
set: (val) => lotteryStore.setWinners(val),
});
const threeContainer = ref(null);
let renderer, scene, camera, animationId;
// let controls; // controls
//
let globalCardIndexes = [];
//
const currentPage = ref(0);
const totalPages = ref(0);
const cardsPerPage = 10; //
let isPageTransitioning = false; //
// 3D
const threeDCards = [];
const targets = {
table: [],
sphere: [],
};
function swapCardContents() {
//
if (threeDCards.length < 2 || globalCardIndexes.length > 0) return;
//
let indexA = Math.floor(Math.random() * threeDCards.length);
let indexB = Math.floor(Math.random() * threeDCards.length);
while (indexA === indexB) {
indexB = Math.floor(Math.random() * threeDCards.length);
}
const cardA = threeDCards[indexA].element;
const cardB = threeDCards[indexB].element;
// ()
if (!cardA.dataset.originalContent) {
cardA.dataset.originalContent = cardA.innerHTML;
}
if (!cardB.dataset.originalContent) {
cardB.dataset.originalContent = cardB.innerHTML;
}
//
[cardA.innerHTML, cardB.innerHTML] = [cardB.innerHTML, cardA.innerHTML];
cardA.classList.add('swap-animation');
cardB.classList.add('swap-animation');
//
setTimeout(() => {
cardA.classList.remove('swap-animation');
cardB.classList.remove('swap-animation');
}, 500);
}
function createElement(css = "", text = "") {
const dom = document.createElement("div");
dom.className = css;
dom.innerHTML = text;
return dom;
}
function createCard(user, isBold, id, showTable, company) {
// 使 CardItem DOM
const container = document.createElement("div");
const app = createApp(CardItem, {
id,
user,
isBold,
showTable,
company,
// highlight, prize
});
app.mount(container);
return container.firstElementChild;
}
function createCards(member, length, showTable, position, config) {
let index = 0;
for (let i = 0; i < config.ROW_COUNT; i++) {
for (let j = 0; j < config.COLUMN_COUNT; j++) {
// 4.
const isBold = (config.HIGHLIGHT_CELL || []).includes(j + "-" + i);
const element = createCard(
member[index % length],
isBold,
index,
showTable,
config.COMPANY
);
const object = new CSS3DObject(element);
object.position.x = Math.random() * 4000 - 2000;
object.position.y = Math.random() * 4000 - 2000;
object.position.z = Math.random() * 4000 - 2000;
scene.add(object);
threeDCards.push(object);
const targetObject = new THREE.Object3D();
targetObject.position.x = j * 140 - position.x;
targetObject.position.y = -(i * 180) + position.y;
targets.table.push(targetObject);
index++;
}
}
}
function createSphereTargets() {
const vector = new THREE.Vector3();
for (let i = 0, l = threeDCards.length; i < l; i++) {
const phi = Math.acos(-1 + (2 * i) / l);
const theta = Math.sqrt(l * Math.PI) * phi;
const object = new THREE.Object3D();
object.position.setFromSphericalCoords(600, phi, theta);
object.position.y -= 200; // 200px
//
vector.set(0, -200, 0); // Y
object.lookAt(vector);
targets.sphere.push(object);
}
}
//
function switchScreen(type) {
if (highlightTimeout) {
clearTimeout(highlightTimeout);
highlightTimeout = null;
}
// enter/table/sphere
if (type === "enter") {
transform(targets.table, 2000, () => {
addHighlight();
highlightTimeout = null;
}); //
} else {
transform(targets.sphere, 2000, () => removeHighlight()); //
}
}
function transform(targetsArr, duration, onComplete) {
for (let i = 0; i < threeDCards.length; i++) {
const object = threeDCards[i];
const target = targetsArr[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())
.onComplete(() => {
if (onComplete) onComplete();
})
.start();
}
function selectCard(selectedCardIndex, currentLuckys, duration = 600) {
if (highlightTimeout) {
clearTimeout(highlightTimeout);
highlightTimeout = null;
}
removeHighlight(); //
console.log("selectCard called:", {
selectedCardIndex,
currentLuckys,
duration,
});
return new Promise((resolve) => {
const width = 140;
//
totalPages.value = Math.ceil(currentLuckys.length / cardsPerPage);
currentPage.value = 0;
//
const pageLocates = [];
for (let page = 0; page < totalPages.value; page++) {
const startIndex = page * cardsPerPage;
const endIndex = Math.min(
(page + 1) * cardsPerPage,
currentLuckys.length
);
const pageCount = endIndex - startIndex;
const pageLocate = [];
//
if (pageCount > 5) {
// 510
const yPosition = [-87, 87];
const mid = Math.ceil(pageCount / 2);
let tag = -(mid - 1) / 2;
for (let i = 0; i < mid; i++) {
pageLocate.push({
x: tag * width,
y: yPosition[0],
});
tag++;
}
tag = -(pageCount - mid - 1) / 2;
for (let i = mid; i < pageCount; i++) {
pageLocate.push({
x: tag * width,
y: yPosition[1],
});
tag++;
}
} else {
// 510
let tag = -(pageCount - 1) / 2;
for (let i = 0; i < pageCount; i++) {
pageLocate.push({
x: tag * width,
y: 0,
});
tag++;
}
}
pageLocates.push(pageLocate);
}
console.log("pageLocates calculated:", pageLocates);
//
selectedCardIndex.forEach((cardIndex, index) => {
changeCard(cardIndex, currentLuckys[index]);
const object = threeDCards[cardIndex];
//
const cardPage = Math.floor(index / cardsPerPage);
const isVisible = cardPage === 0;
//
const pageIndex = index % cardsPerPage;
const pageLocate = pageLocates[cardPage][pageIndex];
//
let initialY;
if (isVisible) {
initialY = pageLocate.y;
} else {
//
initialY = pageLocate.y - 1000;
}
new TWEEN.Tween(object.position)
.to(
{
x: pageLocate.x,
y: initialY,
z: 2200,
},
Math.random() * duration + duration
)
.easing(TWEEN.Easing.Exponential.InOut)
.start();
new TWEEN.Tween(object.rotation)
.to(
{
x: 0,
y: 0,
z: 0,
},
Math.random() * duration + duration
)
.easing(TWEEN.Easing.Exponential.InOut)
.start();
object.element.classList.add("prize");
});
// switchPage使
window.pageLocates = pageLocates;
new TWEEN.Tween({})
.to({}, duration * 2)
.onUpdate(() => render())
.start()
.onComplete(() => {
console.log("selectCard animation completed");
//
if (totalPages.value > 1) {
addWheelListener();
}
resolve();
});
});
}
//
function switchPage(direction) {
if (isPageTransitioning || totalPages.value <= 1) return;
const newPage =
direction === "next" ? currentPage.value + 1 : currentPage.value - 1;
if (newPage < 0 || newPage >= totalPages.value) return;
isPageTransitioning = true;
const duration = 800;
// 使
const pageLocates = window.pageLocates;
if (!pageLocates) {
console.error("页面位置信息未找到");
isPageTransitioning = false;
return;
}
console.log("globalCardIndexes", globalCardIndexes);
//
globalCardIndexes.forEach((cardIndex, index) => {
const object = threeDCards[cardIndex];
const cardPage = Math.floor(index / cardsPerPage);
const isVisible = cardPage === newPage;
const wasVisible = cardPage === currentPage.value;
//
const pageIndex = index % cardsPerPage;
const pageLocate = pageLocates[cardPage][pageIndex];
//
let targetY;
if (isVisible) {
//
if (direction === "next") {
//
targetY = pageLocate.y;
} else {
//
targetY = pageLocate.y;
}
} else {
//
if (direction === "next") {
//
targetY = pageLocate.y + 1000;
} else {
//
targetY = pageLocate.y - 1000;
}
}
//
let startY;
if (wasVisible) {
//
startY = object.position.y;
} else {
//
if (direction === "next") {
//
startY = pageLocate.y - 1000;
} else {
//
startY = pageLocate.y + 1000;
}
}
//
object.position.y = startY;
new TWEEN.Tween(object.position)
.to(
{
x: pageLocate.x,
y: targetY,
z: 2200,
},
duration
)
.easing(TWEEN.Easing.Cubic.InOut)
.start();
});
new TWEEN.Tween({})
.to({}, duration)
.onUpdate(() => render())
.onComplete(() => {
currentPage.value = newPage;
isPageTransitioning = false;
console.log(
`切换到第 ${currentPage.value + 1} 页,共 ${totalPages.value}`
);
})
.start();
}
//
function handleWheel(event) {
if (isPageTransitioning || totalPages.value <= 1) return;
event.preventDefault();
if (event.deltaY > 0) {
//
switchPage("next");
} else if (event.deltaY < 0) {
//
switchPage("prev");
}
}
//
function addWheelListener() {
if (threeContainer.value) {
threeContainer.value.addEventListener("wheel", handleWheel, {
passive: false,
});
}
}
//
function removeWheelListener() {
if (threeContainer.value) {
threeContainer.value.removeEventListener("wheel", handleWheel);
}
}
function resetCard(selectedCardIndex, duration = 500) {
if (!selectedCardIndex || selectedCardIndex.length === 0) {
return Promise.resolve();
}
//
removeWheelListener();
//
currentPage.value = 0;
totalPages.value = 0;
isPageTransitioning = false;
//
if (window.pageLocates) {
delete window.pageLocates;
}
//
globalCardIndexes = [];
selectedCardIndex.forEach((index) => {
const object = threeDCards[index];
const target = targets.sphere[index];
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();
});
return new Promise((resolve) => {
new TWEEN.Tween({})
.to({}, duration * 2)
.onUpdate(() => render())
.start()
.onComplete(() => {
selectedCardIndex.forEach((index) => {
const object = threeDCards[index];
//
if (object.element.dataset.originalContent) {
object.element.innerHTML = object.element.dataset.originalContent;
delete object.element.dataset.originalContent;
}
object.element.classList.remove("prize");
});
resolve();
});
});
}
function changeCard(cardIndex, user) {
//
if (!globalCardIndexes.includes(cardIndex)) {
globalCardIndexes.push(cardIndex);
}
const card = threeDCards[cardIndex].element;
// 便
if (!card.dataset.originalContent) {
card.dataset.originalContent = card.innerHTML;
}
// -
// : { jwcode: "5412", username: "22" }
const jwcode = user.jwcode || user[0] || "";
const username = user.username || user[1] || "";
const company = user.company || user[2] || "PSST";
card.innerHTML = `<div style="font-size: 30px; font-weight: bold; color: #ffffff; text-align: center; display: flex; justify-content: center; align-items: center; width: 100%; height: 100%;">${jwcode}</div>`;
//
card.classList.add("prize");
}
function changeCard1() {
console.log("执行取消高光,当前卡片索引:", globalCardIndexes);
//
removeWheelListener();
//
currentPage.value = 0;
totalPages.value = 0;
isPageTransitioning = false;
//
if (window.pageLocates) {
delete window.pageLocates;
}
globalCardIndexes.forEach((cardIndex) => {
const card = threeDCards[cardIndex].element;
// console.log('', cardIndex, '');
//
if (card.dataset.originalContent) {
card.innerHTML = card.dataset.originalContent;
delete card.dataset.originalContent;
}
// prizeCardItem
card.classList.remove("prize");
});
//
globalCardIndexes = [];
}
function shine(cardIndex, color) {
const card = threeDCards[cardIndex].element;
card.style.backgroundColor =
color || `rgba(0,127,127,${Math.random() * 0.7 + 0.25})`;
}
//
const highlightedIndexes = ref([]);
// addHighlight removeHighlight
function addHighlight(indexes = null) {
if (indexes) {
highlightedIndexes.value = [...indexes];
} else {
// .lightitem
highlightedIndexes.value = threeDCards
// .map((obj, idx) =>
// obj.element.classList.contains("lightitem") ? idx : null
// )
.filter((idx) => idx !== null);
}
}
function removeHighlight(indexes = null) {
if (indexes) {
highlightedIndexes.value = highlightedIndexes.value.filter(
(i) => !indexes.includes(i)
);
} else {
highlightedIndexes.value = [];
}
}
// DOM
watch(highlightedIndexes, (newVal) => {
threeDCards.forEach((cardObj, idx) => {
if (newVal.includes(idx)) {
cardObj.element.classList.add("highlight");
} else {
cardObj.element.classList.remove("highlight");
}
});
});
let rotateObj = null;
let highlightTimeout = null;
function rotateBallStart() {
return new Promise((resolve) => {
if (!scene) return resolve();
scene.rotation.y = 0;
rotateObj = new TWEEN.Tween(scene.rotation)
.to({ y: Math.PI * 6 * 1000 }, 3000 * 1000)
.onUpdate(() => render())
.onComplete(() => resolve())
.start();
});
}
function rotateBallStop() {
return new Promise((resolve) => {
if (!scene || !rotateObj) return resolve();
rotateObj.stop();
//
const currentY = scene.rotation.y;
const targetY = Math.ceil(currentY / (2 * Math.PI)) * 2 * Math.PI;
const deltaY = Math.abs(targetY - currentY);
const duration = 500 + 1000 * (deltaY / Math.PI);
new TWEEN.Tween(scene.rotation)
.to({ y: targetY }, duration)
.easing(TWEEN.Easing.Cubic.Out)
.onUpdate(() => render())
.onComplete(() => {
scene.rotation.y = 0;
render();
resolve();
})
.start();
});
}
function getTotalCards() {
return threeDCards.length;
}
onMounted(async () => {
// 3D
scene = new THREE.Scene();
camera = new THREE.PerspectiveCamera(
40,
window.innerWidth / window.innerHeight,
1,
10000
);
camera.position.z = 3000;
// camera.position.y = 250; // 10px
//
// scene.position.y = 10; // 10px
renderer = new CSS3DRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
if (threeContainer.value) {
threeContainer.value.appendChild(renderer.domElement);
}
// 2. 8
let text = "0123";
let step = 5;
let xoffset = 1;
let yoffset = 1;
let highlight = [];
text.split("").forEach((n) => {
highlight = highlight.concat(
NUMBER_MATRIX[n].map((item) => {
return `${item[0] + xoffset}-${item[1] + yoffset}`;
})
);
xoffset += step;
});
// const highlightCells = NUMBER_MATRIX["0"].map(([x, y]) => `${x}-${y}`);
const config = {
ROW_COUNT: 7, // 5
COLUMN_COUNT: 21, // 4
HIGHLIGHT_CELL: highlight,
COMPANY: "演示公司",
};
const userList = await getUserListApi();
console.log("3D调用一次接口", userList);
// lotteryStore.setWinners(userList);
// console.log("userList", userList);
// 3D
const member = userList.data.map((item) => [
item.jwcode,
item.username,
"PSST",
]);
// store
const userNames = userList.data.map(
(item) => item.jwcode || item.username || ""
);
lotteryStore.setAllUsers(userNames);
const length = member.length;
const showTable = true;
const position = {
x: (100 * config.COLUMN_COUNT - 20) / 2,
y: (120 * config.ROW_COUNT - 20) / 2,
};
createCards(member, length, showTable, position, config); // 3.
createSphereTargets();
//
render();
animate();
//
setTimeout(() => {
switchScreen("enter");
}, 500);
window.addEventListener("resize", onWindowResize);
// swapInterval.value = setInterval(swapCardContents, swapIntervalTime.value);
});
onBeforeUnmount(() => {
window.removeEventListener("resize", onWindowResize);
removeWheelListener(); //
if (animationId) cancelAnimationFrame(animationId);
if (highlightTimeout) {
clearTimeout(highlightTimeout);
highlightTimeout = null;
}
// if (swapInterval.value) {
// clearInterval(swapInterval.value);
// swapInterval.value = null;
// }
});
function render() {
renderer.render(scene, camera);
}
function animate() {
animationId = requestAnimationFrame(animate);
TWEEN.update();
// controls.update(); // controls
render();
}
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
render();
}
defineExpose({
resetCard,
addHighlight,
switchScreen,
rotateBallStart,
rotateBallStop,
selectCard,
getTotalCards,
changeCard1,
});
</script>
<style scoped>
.lottery-3d-container {
width: 100vw;
height: 100vh;
overflow: hidden;
position: relative;
}
.three-container {
width: 100%;
height: 100%;
}
/* 分页指示器样式 */
.page-indicator {
position: absolute;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
z-index: 1000;
background: rgba(0, 0, 0, 0.7);
color: white;
padding: 8px 16px;
border-radius: 20px;
font-size: 14px;
pointer-events: none;
}
/* 滚动提示样式 */
.scroll-hint {
position: absolute;
bottom: 60px;
left: 50%;
transform: translateX(-50%);
z-index: 1000;
background: rgba(0, 0, 0, 0.7);
color: white;
padding: 8px 16px;
border-radius: 20px;
font-size: 12px;
pointer-events: none;
animation: fadeInOut 2s ease-in-out infinite;
}
.price-font {
font-size: 16px;
font-weight: bold;
color: #ffffff;
text-align: center;
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
}
@keyframes fadeInOut {
0%,
100% {
opacity: 0.6;
}
50% {
opacity: 1;
}
}
</style>

54
src/views/choujiang/lottery/Mascot.vue

@ -0,0 +1,54 @@
<template>
<div class="mascot-container">
<img
src="../../../assets/qilin.webp"
alt="可爱的角色"
class="mascot-image"
/>
</div>
</template>
<script setup>
//
</script>
<style scoped>
.mascot-container {
position: fixed;
bottom: -10px;
right: -80px;
z-index: 1000;
pointer-events: none;
}
.mascot-image {
width: 400px;
height: 400px;
animation: bounce 2s ease-in-out infinite;
}
/* @keyframes bounce {
0%, 20%, 50%, 80%, 100% {
transform: translateY(0);
}
40% {
transform: translateY(-10px);
}
60% {
transform: translateY(-5px);
}
} */
/* 响应式设计 */
@media (max-width: 768px) {
.mascot-image {
width: 180px;
height: 180px;
}
.mascot-container {
bottom: 10px;
right: 10px;
}
}
</style>

76
src/views/choujiang/lottery/MusicPlayer.vue

@ -0,0 +1,76 @@
<template>
<div class="music-box" @click="toggleMusic">
<audio ref="audioRef" :src="musicSrc" loop></audio>
<div class="music-icon" :class="{ playing }"></div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue';
const musicSrc = '/src/assets/music.mp3';
const audioRef = ref(null);
const playing = ref(false);
function toggleMusic() {
if (!audioRef.value) return;
if (audioRef.value.paused) {
audioRef.value.play();
playing.value = true;
} else {
audioRef.value.pause();
playing.value = false;
}
}
//
function isPlaying() {
return playing.value;
}
//
onMounted(() => {
//
setTimeout(() => {
if (audioRef.value) {
audioRef.value.play().then(() => {
playing.value = true;
}).catch((error) => {
console.log('自动播放失败,可能需要用户交互:', error);
});
}
}, 500);
});
//
defineExpose({
toggleMusic,
isPlaying
});
</script>
<style scoped>
.music-box {
position: absolute;
top: 24px;
right: 32px;
width: 48px;
height: 48px;
z-index: 20;
cursor: pointer;
}
.music-icon {
width: 100%;
height: 100%;
background: url('/src/assets/lottery/edifier.jpg') no-repeat center/cover;
border-radius: 50%;
transition: box-shadow 0.2s;
}
.music-icon.playing {
animation: rotate 1.2s linear infinite;
/* box-shadow: 0 0 12px #0078ff; */
}
@keyframes rotate {
100% { transform: rotate(360deg); }
}
</style>

716
src/views/choujiang/lottery/PrizePanel.vue

@ -0,0 +1,716 @@
<template>
<div v-if="showOne">
<div class="prize-panel-root">
<div class="prize-panel-list" v-if="prizes && prizes.length">
<div
class="prize-panel-item"
v-for="(prize, idx) in prizes"
:key="prize.type || idx"
:class="{
'revealed-highlight': idx === lastRevealedIdx,
disabled: idx === nextRevealIdx && !canRevealPrize(idx),
}"
@click="handleReveal(idx)"
:style="{
cursor:
idx === nextRevealIdx && !canRevealPrize(idx)
? 'not-allowed'
: 'pointer',
}"
>
<div v-if="isRevealed(idx)" class="prize-card">
<div class="prize-img-wrap">
<img class="prize-img" :src="prize.img" :alt="prize.title" />
</div>
<div class="prize-info">
<div class="prize-row prize-row-top">
<span class="prize-level">{{ prize.title }}</span>
<span class="prize-name">{{ prize.text }}</span>
</div>
<div class="prize-row prize-row-bottom">
<div class="progress-bar-bg">
<div
class="progress-bar-fill"
:style="{ width: getProgressPercent(prize) + '%' }"
></div>
<span class="progress-bar-text">
{{ getLeftCount(prize) }}/{{ prize.count }}
</span>
</div>
</div>
</div>
</div>
<div v-else class="prize-card prize-card-mask">
<img
src="../../../assets/daijiemi.png"
alt="待揭秘"
class="prize-mask-img"
/>
</div>
</div>
<div></div>
<div></div>
<div></div>
<div></div>
</div>
<div class="prize-panel-footer">
<div class="arrow-up" @click="openWinnerList"></div>
<button
ref="winnerBtnRef"
class="winner-btn"
@click="toggleWinnerList"
>
获奖名单
</button>
</div>
</div>
</div>
<div v-else>
<div class="prize-panel-root">
<div
class="prize-panel-list1"
v-if="prizes && prizes.length && lastRevealedIdx >= 0"
>
<div
class="prize-panel-item"
:key="prizes[lastRevealedIdx].type || lastRevealedIdx"
:class="{ 'revealed-highlight': true }"
style="cursor: pointer"
>
<div class="prize-card">
<div class="prize-img-wrap">
<img
class="prize-img"
:src="prizes[lastRevealedIdx].img"
:alt="prizes[lastRevealedIdx].title"
/>
</div>
<div class="prize-info">
<div class="prize-row prize-row-top">
<span class="prize-level">{{
prizes[lastRevealedIdx].title
}}</span>
<span class="prize-name">{{
prizes[lastRevealedIdx].text
}}</span>
</div>
<div class="prize-row prize-row-bottom">
<div class="progress-bar-bg">
<div
class="progress-bar-fill"
:style="{
width: getProgressPercent(prizes[lastRevealedIdx]) + '%',
}"
></div>
<span class="progress-bar-text">
{{ getLeftCount(prizes[lastRevealedIdx]) }}/{{
prizes[lastRevealedIdx].count
}}
</span>
</div>
<div class="prize-panel-footer">
<div class="arrow-down" @click="toggleWinnerList"></div>
<div
v-if="showWinnerList"
class="winner-modal-mask"
@click="closeWinnerList"
>
<div
class="winner-modal"
:style="{
position: 'absolute',
left: modalLeft + 'px',
top: modalTop + 'px',
}"
@click.stop
>
<div class="winner-modal-title">
<span>Homily ID</span><span>奖项</span>
</div>
<div
style="
background: linear-gradient(
to left,
rgb(232 76 10),
rgb(195 6 6),
rgb(240 90 9)
);
height: 1px;
"
></div>
<ul class="winner-list">
<li
v-for="(user, idx) in fakeWinners"
:key="idx"
style="
display: flex;
justify-content: space-between;
align-items: center;
"
>
<!-- <span>{{ user.id }}</span> - <span>{{ user.name }}</span> - -->
<span>{{ user.jwcode }}</span>
<span>{{ user.prizeName }}</span>
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, nextTick, watch, onMounted } from "vue";
import { useLotteryStore } from "../../../store/lottery";
import { useDataManager } from "../lottery/dataManager";
const props = defineProps({
prizes: Array,
});
//
const revealedCount = ref(0);
//
const lotteryStore = useLotteryStore();
const lastRevealed = computed({
get: () => lotteryStore.lastRevealedIdx,
set: (val) => lotteryStore.setLastRevealedIdx(val),
});
const waitingForNextReveal = computed({
get: () => lotteryStore.waitingForNextReveal,
set: (val) => lotteryStore.setWaitingForNextReveal(val),
});
const winners = computed({
get: () => lotteryStore.winners,
set: (val) => lotteryStore.setWinners(val),
});
// watchwinners
// watch(winners, (newVal) => {
// console.log("winners", newVal);
// fakeWinners.value = newVal;
// });
const lastRevealedIdx = ref(-1);
lastRevealedIdx.value = lastRevealed.value;
const showOne = ref(true);
//
const isRevealed = (idx) =>
idx >= (props.prizes?.length || 0) - revealedCount.value;
// index
const nextRevealIdx = computed(
() => (props.prizes?.length || 0) - revealedCount.value - 1
);
//
function canRevealPrize(idx) {
//
if (lastRevealedIdx.value === -1) {
return true;
}
//
const lastPrize = props.prizes[lastRevealedIdx.value];
if (lastPrize) {
const leftCount = getLeftCount(lastPrize);
//
if (leftCount > 0) {
waitingForNextReveal.value = false;
return false;
}
waitingForNextReveal.value = true;
}
return true;
}
//
function handleReveal(idx) {
//
if (idx !== nextRevealIdx.value) {
alert("请按顺序揭秘奖品!");
return;
}
if (idx === nextRevealIdx.value && canRevealPrize(idx)) {
revealedCount.value++;
lastRevealedIdx.value = idx; //
if (idx === 0) {
waitingForNextReveal.value = false;
}
console.log("lastRevealedIdx.value", lastRevealedIdx.value);
lastRevealed.value = idx;
console.log("lastRevealed.value", lastRevealed.value);
} else if (idx === nextRevealIdx.value && !canRevealPrize(idx)) {
//
console.log("上一个奖品还未抽完,不能揭秘下一个奖品");
// toast
// alert("");
}
}
//
function getLeftCount(prize) {
// type dataManager.state.basicData.luckyUsers
// luckyUsers 访
// window.dataManager
let luckyUsers =
(window.dataManager && window.dataManager.state.basicData.luckyUsers) || {};
let got = luckyUsers[prize.type]?.length || 0;
return prize.remainNum;
}
//
const showWinnerList = ref(false);
const fakeWinners = ref([]);
// fakeWinners.value = winners.value;
console.log("fakeWinners", fakeWinners.value);
import { getGetPrizeUserListApi } from "../../../api/API";
const updateWinners = async () => {
try {
const response = await getGetPrizeUserListApi({
});
console.log("updatePrizeList response", response);
fakeWinners.value = response.data.list;
console.log("updateWinners fakeWinners", fakeWinners.value);
} catch (error) {
console.error("updatePrizeList error", error);
}
};
async function openWinnerList() {
// showWinnerList.value = true;
if (!showWinnerList.value) {
if (revealedCount.value === 0) {
alert("请先揭晓奖品,并抽奖!");
}
// await updatePrizeList();
}
if (revealedCount.value > 0) {
showWinnerList.value = true;
}
// showOnefalse
if (lastRevealedIdx.value >= 0) {
showOne.value = false;
}
}
function closeWinnerList() {
showWinnerList.value = false;
// showOnefalsetrue
if (!showOne.value) {
showOne.value = true;
}
}
const winnerBtnRef = ref(null);
const modalLeft = ref(0);
const modalTop = ref(0);
async function toggleWinnerList() {
await updateWinners();
showWinnerList.value = !showWinnerList.value;
console.log(
"toggleWinnerList - showWinnerList:",
showWinnerList.value,
"showOne:",
showOne.value,
"lastRevealedIdx:",
lastRevealedIdx.value
);
if (!showWinnerList.value) {
if (revealedCount.value === 0) {
alert("请先揭晓奖品,并抽奖!");
}
}
if (showWinnerList.value) {
// showOnefalse
if (lastRevealedIdx.value > 0) {
showOne.value = false;
console.log("设置 showOne 为 false");
}
} else {
// showOnefalsetrue
if (!showOne.value) {
showOne.value = true;
console.log("设置 showOne 为 true");
}
}
}
function getProgressPercent(prize) {
const total = prize.count || 1;
const left = getLeftCount(prize);
//
return Math.round((left / total) * 100);
}
// onMounted(() => {
// updateWinners();
// });
</script>
<style scoped>
.prize-panel-list {
position: relative;
background: none;
z-index: 10;
min-width: 300px;
max-width: 342px;
text-align: left;
display: flex;
flex-direction: column;
gap: 18px;
height: 700px;
overflow-x: hidden !important;
overflow-y: auto;
padding-right: 10px;
scrollbar-width: thin;
scrollbar-color: #ffd283 rgba(255, 210, 131, 0.3); /* Firefox */
}
.prize-panel-list1 {
position: relative;
background: none;
z-index: 10;
min-width: 320px;
max-width: 342px;
text-align: left;
display: flex;
}
.prize-panel-list::-webkit-scrollbar {
width: 6px;
}
.prize-panel-list::-webkit-scrollbar-track {
background: rgba(255, 210, 131, 0.3);
border-radius: 3px;
}
.prize-panel-list::-webkit-scrollbar-thumb {
background-color: #ffd283;
border-radius: 3px;
}
.prize-panel-item {
background: #ffd283;
border-radius: 6px 6px 6px 6px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
display: flex;
align-items: center;
min-width: 320px;
}
.prize-card {
display: flex;
align-items: center;
width: 100%;
padding: 10px 18px;
}
.prize-img-wrap {
width: 64px;
height: 64px;
border-radius: 50%;
background: #fff;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
margin-right: 18px;
border: 2px solid #fff3e0;
}
.prize-img {
width: 60px;
height: 60px;
object-fit: contain;
}
.prize-info {
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
}
.prize-row {
display: flex;
align-items: center;
background: #ffffff;
border-radius: 93px 93px 93px 93px;
}
.prize-row-top {
margin-bottom: 8px;
border: 1px solid #ea2b0a;
}
.prize-level {
background: linear-gradient(90deg, #ff9800 0%, #ff5722 100%);
color: #fff;
border-radius: 15.71px 15.71px 15.71px 15.71px;
padding: 2px 18px;
font-size: 18px;
font-weight: bold;
margin-right: 12px;
}
.prize-name {
font-size: 18px;
color: #d84315;
font-weight: 500;
}
/* .prize-row-bottom {
background: linear-gradient(90deg, #ff9800 0%, #ff5722 100%);
background: #8a3500;
border-radius: 16px;
color: #fff;
font-size: 20px;
font-weight: bold;
padding: 2px 0 2px 0;
justify-content: center;
min-width: 80px;
} */
.custom-arrow-icon {
font-size: 24px; /* 图标大小 */
color: #d84315; /* 图标颜色,使用项目主题橙色 */
margin: 5px; /* 外边距 */
cursor: pointer; /* 鼠标悬停样式 */
transition: transform 0.3s ease; /* 过渡动画 */
}
.custom-arrow-icon:hover {
transform: scale(1.1); /* 悬停放大效果 */
}
.prize-count {
font-size: 20px;
font-weight: bold;
}
.prize-divider {
margin: 0 4px;
font-size: 20px;
}
.prize-total {
font-size: 20px;
font-weight: bold;
}
.prize-panel-root {
position: absolute;
top: 20px;
left: 20px;
background: none;
z-index: 10;
min-width: 320px;
max-width: 342px;
display: flex;
flex-direction: column;
}
.prize-panel-footer {
position: relative;
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
z-index: 20;
padding: 10px 0;
border-radius: 0 0 6px 6px;
margin-top: 10px;
}
.arrow-up {
position: relative;
width: 50px;
height: 30px;
margin-bottom: 4px;
cursor: pointer;
background-image: url("../../../assets/展开.png");
background-size: cover;
background-position: center;
transform: rotate(180deg);
}
.arrow-down {
position: fixed;
top: 100px;
left: 150px;
width: 47px;
height: 28px;
margin-bottom: 4px;
cursor: pointer;
background-image: url("../../../assets/展开.png");
background-size: cover;
background-position: center;
}
.winner-btn {
background: rgba(255, 210, 131, 0.8);
color: #d5291f;
border: #fff;
border-radius: 8px;
padding: 15px 79px;
font-size: 20px;
font-weight: bold;
cursor: pointer;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}
.winner-modal-mask {
position: fixed;
top: 142px;
left: -4px;
width: 100vw;
height: 100vh;
background: rgba(0, 0, 0, 0.01);
z-index: 1000;
display: flex;
align-items: flex-start;
justify-content: center;
}
.winner-modal {
background: rgba(255, 210, 131, 0.8);
border-radius: 12px;
padding-top: 12px;
padding-left: 25px;
padding-right: 25px;
padding-bottom: 10px;
min-width: 280px;
max-width: 90vw;
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.12);
position: relative;
margin-left: 10px;
}
.winner-modal-title {
font-size: 22px;
font-weight: bold;
color: #e64f39;
margin-bottom: 5px;
text-align: center;
display: flex;
justify-content: space-between; /* 左右对齐 */
align-items: center; /* 垂直居中对齐 */
/* 可添加padding或margin调整整体间距 */
padding: 5px 0;
}
.winner-modal-close {
position: absolute;
right: 18px;
top: 12px;
font-size: 26px;
color: #888;
cursor: pointer;
}
.winner-list {
max-height: 260px;
/* background: rgba(255, 210, 131, 0.8);/ */
overflow-y: auto;
padding: 0;
margin: 0;
list-style: none;
/* 隐藏滚动条但保留滚动功能 */
scrollbar-width: none; /* Firefox */
-ms-overflow-style: none; /* IE and Edge */
}
.winner-list::-webkit-scrollbar {
display: none; /* Chrome, Safari and Opera */
}
.winner-list li {
padding: 8px 0;
/* border-bottom: 1px solid #f2f2f2; */
font-size: 17px;
color: #d84315;
display: flex;
gap: 12px;
align-items: center;
justify-content: center;
text-align: center;
}
.progress-bar-bg {
position: relative;
width: 220px;
height: 28px;
background: #e9620e;
border-radius: 16px;
overflow: hidden;
display: flex;
align-items: center;
margin: 0 auto;
border: #e13726;
}
.progress-bar-fill {
position: absolute;
left: 0;
top: 0;
height: 100%;
/* background: linear-gradient(90deg, #ff9800 0%, #8a3500 100%); */
background: #8a3500;
border-radius: 16px;
transition: width 0.4s;
z-index: 1;
}
.progress-bar-text {
position: relative;
width: 100%;
text-align: center;
color: #ffffff;
font-size: 18px;
font-weight: bold;
z-index: 2;
letter-spacing: 1px;
}
.prize-card-mask {
position: relative;
width: 342px;
height: 88px;
display: flex;
/* align-items: center;
justify-content: center; */
padding: 0;
overflow: hidden;
}
.prize-mask-img {
object-fit: cover;
position: absolute;
width: 100%;
height: 98%;
object-fit: cover;
left: 0;
top: 0;
border-radius: 8px 8px 8px 8px;
}
.prize-panel-item.revealed-highlight {
/* border: 3px solid #ff9800; */
box-shadow: 0 0 16px 4px #ff9800aa;
transform: scale(1.05);
z-index: 2;
transition: all 0.3s;
}
.prize-panel-item.disabled {
cursor: not-allowed !important;
position: relative;
}
.prize-panel-item.disabled::after {
content: "请先抽完上一个奖品";
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: rgba(0, 0, 0, 0.8);
color: white;
padding: 8px 12px;
border-radius: 6px;
font-size: 14px;
white-space: nowrap;
z-index: 10;
opacity: 0;
transition: opacity 0.3s;
pointer-events: none;
}
.prize-panel-item.disabled:hover::after {
opacity: 1;
}
</style>

37
src/views/choujiang/lottery/Qipao.vue

@ -0,0 +1,37 @@
<template>
<div v-if="show" class="qipao" :style="styleObj">
{{ text }}
</div>
</template>
<script setup>
import { ref, watch, computed } from 'vue';
const props = defineProps({
text: String,
show: Boolean
});
const styleObj = computed(() => ({
position: 'absolute',
background: 'rgba(0,0,0,0.8)',
color: '#fff',
padding: '8px 12px',
borderRadius: '4px',
fontSize: '14px',
left: Math.random() * (window.innerWidth - 200) + 'px',
top: Math.random() * (window.innerHeight - 100) + 'px',
zIndex: 1000,
animation: 'fadeInOut 3s ease-in-out forwards'
}));
</script>
<style scoped>
.qipao {
white-space: nowrap;
}
@keyframes fadeInOut {
0% { opacity: 0; }
10% { opacity: 1; }
90% { opacity: 1; }
100% { opacity: 0; }
}
</style>

62
src/views/choujiang/lottery/UserList.vue

@ -0,0 +1,62 @@
<template>
<div class="user-list-panel">
<div class="lucky-list">
<div class="list-title">中奖名单</div>
<ul>
<li v-for="(user, idx) in luckyUsers" :key="idx">{{ user[1] }}</li>
</ul>
</div>
<!-- <div class="left-list">
<div class="list-title">未中奖用户</div>
<ul>
<li v-for="(user, idx) in leftUsers" :key="idx">{{ user[1] }}</li>
</ul>
</div> -->
</div>
</template>
<script setup>
import { useLotteryStore } from "../../../store/lottery";
const lotteryStore = useLotteryStore();
const props = defineProps({
luckyUsers: {
type: Array,
default: () => []
},
leftUsers: {
type: Array,
default: () => []
}
});
</script>
<style scoped>
.user-list-panel {
position: absolute;
right: 24px;
top: 80px;
width: 220px;
background: rgba(255,255,255,0.95);
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
z-index: 15;
padding: 16px 12px;
font-size: 15px;
}
.list-title {
font-weight: bold;
margin-bottom: 6px;
font-size: 16px;
}
ul {
margin: 0 0 12px 0;
padding: 0;
list-style: none;
}
li {
padding: 2px 0;
border-bottom: 1px dashed #eee;
}
</style>

217
src/views/choujiang/lottery/dataManager.js

@ -0,0 +1,217 @@
import { reactive } from 'vue';
import { getPrizeListApi, getUserListApi } from '../../../api/API';
export function useDataManager() {
const state = reactive({
basicData: {
prizes: [],
users: [],
luckyUsers: {},
leftUsers: []
},
currentPrizeIndex: 0,
currentPrize: null,
currentLuckys: [],
isLotting: false,
// 新增:轮次管理
currentRound: 1,
config: {
prizes: [],
EACH_COUNT: [],
COMPANY: '',
HIGHLIGHT_CELL: [],
Resolution: 1,
ROW_COUNT: 7,
COLUMN_COUNT: 20
}
});
async function getBasicData() {
// 获取奖品列表
const prizeList = await getPrizeListApi();
const fakePrizes = prizeList.data.map((item, index) => ({
type: index, // 使用索引作为type
title: item.gradeName,
text: item.prizeName,
count: item.amount,
img: item.imageUrl,
gradeId: item.gradeId,
prizeId: item.prizeId,
remainNum: item.remainNum,
perWin: item.perWin
}));
const fakeEachCount = prizeList.data.map(item => item.perWin);
console.log("fakeEachCount", fakeEachCount);
const fakeCompany = '前端假公司';
const fakeLuckyData = {};
fakePrizes.forEach((prize, index) => {
fakeLuckyData[index] = [];
});
// 获取真实用户数据
const userListResponse = await getUserListApi();
console.log("dataManager调用一次接口", userListResponse);
console.log("userList", userListResponse);
// 将后端返回的用户数据转换为兼容格式
const realUsers = userListResponse.data.map(item => ({
jwcode: item.jwcode,
username: item.username,
company: fakeCompany // 使用默认公司名称
}));
state.config.prizes = fakePrizes;
state.config.EACH_COUNT = fakeEachCount;
state.config.COMPANY = fakeCompany;
state.config.HIGHLIGHT_CELL = [];
state.basicData.prizes = fakePrizes;
state.basicData.users = realUsers; // 使用真实用户数据
state.basicData.leftUsers = realUsers.slice(); // 初始化剩余用户为所有用户
state.basicData.luckyUsers = fakeLuckyData;
determineCurrentPrize();
return Promise.resolve({
cfgData: {
prizes: fakePrizes,
EACH_COUNT: fakeEachCount,
COMPANY: fakeCompany
},
leftUsers: realUsers.slice(),
luckyData: fakeLuckyData
});
}
async function getUsers() {
const userList = await getUserListApi();
console.log("userList", userList);
// 将后端返回的用户数据转换为兼容格式
const realUsers = userList.data.map(item => ({
jwcode: item.jwcode,
username: item.username,
company: state.config.COMPANY || '前端假公司'
}));
state.basicData.users = realUsers;
return Promise.resolve(userList);
}
function determineCurrentPrize() {
let prizeIndex = state.basicData.prizes.length - 1;
for (; prizeIndex > -1; prizeIndex--) {
if (
state.basicData.luckyUsers[prizeIndex] &&
state.basicData.luckyUsers[prizeIndex].length >=
state.basicData.prizes[prizeIndex].count
) {
continue;
}
state.currentPrizeIndex = prizeIndex;
state.currentPrize = state.basicData.prizes[state.currentPrizeIndex];
break;
}
}
async function saveData(type, data) {
return Promise.resolve();
}
async function setErrorData(data) { return Promise.resolve(); }
async function exportData() { alert('导出功能为假数据模式,无实际导出。'); return Promise.resolve(); }
async function resetData() { return Promise.resolve(); }
function getTotalCards() {
return state.config.ROW_COUNT * state.config.COLUMN_COUNT || 50;
}
function setLotteryStatus(status = false) { state.isLotting = status; }
function resetAllData() {
state.basicData.luckyUsers = {};
state.basicData.leftUsers = state.basicData.users.slice();
state.currentLuckys = [];
determineCurrentPrize();
}
function updateCurrentPrize() { determineCurrentPrize(); }
// 新增:计算当前奖品的总轮次
function getTotalRounds(prizeIndex) {
const prize = state.basicData.prizes[prizeIndex];
const eachCount = state.config.EACH_COUNT[prizeIndex];
if (!prize || !eachCount) return 0;
return Math.ceil(prize.count / eachCount);
}
// 新增:计算当前轮次
function getCurrentRound(prizeIndex) {
const luckyUsers = state.basicData.luckyUsers[prizeIndex] || [];
const eachCount = state.config.EACH_COUNT[prizeIndex];
if (!eachCount) return 1;
return Math.floor(luckyUsers.length / eachCount) + 1;
}
// 新增:计算剩余数量
function getLeftCount(prizeIndex) {
const prize = state.basicData.prizes[prizeIndex];
const luckyUsers = state.basicData.luckyUsers[prizeIndex] || [];
if (!prize) return 0;
return prize.count - luckyUsers.length;
}
// 新增:更新当前轮次
function updateCurrentRound() {
state.currentRound = getCurrentRound(state.currentPrizeIndex);
}
// 新增:更新奖品列表数据
async function updatePrizeList() {
try {
console.log('开始更新奖品列表数据...');
const prizeList = await getPrizeListApi();
const fakePrizes = prizeList.data.map((item, index) => ({
type: index, // 使用索引作为type
title: item.gradeName,
text: item.prizeName,
count: item.amount,
img: item.imageUrl,
gradeId: item.gradeId,
prizeId: item.prizeId,
remainNum: item.remainNum,
perWin: item.perWin
}));
const fakeEachCount = prizeList.data.map(item => item.perWin);
console.log("更新后的奖品列表:", fakePrizes);
console.log("更新后的每轮数量:", fakeEachCount);
// 更新配置数据
state.config.prizes = fakePrizes;
state.config.EACH_COUNT = fakeEachCount;
state.basicData.prizes = fakePrizes;
// 重新确定当前奖品
determineCurrentPrize();
console.log('奖品列表数据更新完成');
return Promise.resolve(fakePrizes);
} catch (error) {
console.error('更新奖品列表失败:', error);
return Promise.reject(error);
}
}
return {
state,
getBasicData,
getUsers,
saveData,
setErrorData,
exportData,
resetData,
getTotalCards,
setLotteryStatus,
resetAllData,
updateCurrentPrize,
getTotalRounds,
getCurrentRound,
getLeftCount,
updateCurrentRound,
updatePrizeList
};
}

198
src/views/choujiang/lottery/lotteryEngine.js

@ -0,0 +1,198 @@
import { ref, computed, watch } from 'vue';
import { useLotteryStore } from '../../../store/lottery' // 路径根据实际情况调整
import { drawLottery } from '../../../api/API'; // 导入新的抽奖接口
function getRandomInt(max) {
return Math.floor(Math.random() * max);
}
const lotteryStore = useLotteryStore();
// const winners = computed({
// get: () => lotteryStore.winners,
// set: (val) => lotteryStore.setWinners(val),
// });
const lastRevealed = computed({
get: () => lotteryStore.lastRevealedIdx,
set: (val) => lotteryStore.setLastRevealedIdx(val),
});
const winnerList = computed({
get: () => lotteryStore.winnerList,
set: (val) => lotteryStore.setWinnerList(val),
});
// 用watch监听winners的变化
// watch(winners, (newVal) => {
// console.log('winners', newVal);
// winners.value = newVal;
// });
export function useLotteryEngine(dataManager, renderer3D) {
const isLotting = ref(false);
const lotteryStore = useLotteryStore(); // 只获取一次
async function executeLottery() {
if (isLotting.value) return;
isLotting.value = true;
dataManager.setLotteryStatus(true);
await dataManager.saveData();
changePrize();
// 重置卡片动画
await renderer3D.resetCard([]);
// 计算本次应该抽奖的人数
const currentPrizeIndex = dataManager.state.currentPrizeIndex;
const prize = dataManager.state.basicData.prizes[lastRevealed.value];
const luckyUsers = dataManager.state.basicData.luckyUsers[lastRevealed.value] || [];
const remainingPrizeCount = prize.count - luckyUsers.length; // 奖品剩余数量
const basePerCount = dataManager.state.config.EACH_COUNT[lastRevealed.value] || 1;
const actualPerCount = Math.min(basePerCount, remainingPrizeCount); // 取最小值
console.log('executeLottery - currentPrizeIndex:', lastRevealed.value, 'prize:', prize, 'basePerCount:', basePerCount, 'remainingPrizeCount:', remainingPrizeCount, 'actualPerCount:', actualPerCount);
// 请求后端进行抽奖
try {
const lotteryData = {
gradeId: prize.gradeId,
prizeId: prize.prizeId,
perWin: basePerCount,
remainNum: prize.remainNum
};
// console.log('请求后端抽奖,参数:', lotteryData);
const response = winnerList.value
console.log('后端抽奖返回结果:', response);
// winners.value = response.data.data;
// console.log('抽奖的winners', winners.value);
// winners.value = response.data;
// console.log('后端抽奖返回结果:', response);
if (response && response.data.data && Array.isArray(response.data.data)) {
// 后端返回中奖用户数据
const currentLuckys = response.data.data.map(item => ({
jwcode: item.jwcode,
username: item.username
}));
console.log('后端返回的中奖用户:', currentLuckys);
// 生成随机卡片索引用于显示
const totalCards = dataManager.getTotalCards();
let selectedCardIndex = [];
for (let i = 0; i < currentLuckys.length; i++) {
let cardIndex = getRandomInt(totalCards);
while (selectedCardIndex.includes(cardIndex)) {
cardIndex = getRandomInt(totalCards);
}
selectedCardIndex.push(cardIndex);
}
console.log('executeLottery - selectedCardIndex:', selectedCardIndex, 'currentLuckys:', currentLuckys);
dataManager.state.currentLuckys = currentLuckys;
// 保存中奖用户到对应奖品
if (!dataManager.state.basicData.luckyUsers[currentPrizeIndex]) {
dataManager.state.basicData.luckyUsers[currentPrizeIndex] = [];
}
dataManager.state.basicData.luckyUsers[currentPrizeIndex].push(...currentLuckys);
// 更新轮次信息
dataManager.updateCurrentRound();
// 展示中奖动画
console.log('executeLottery - calling selectCard');
await renderer3D.selectCard?.(selectedCardIndex, currentLuckys);
console.log('executeLottery - selectCard completed');
} else {
console.error('后端抽奖返回数据格式错误:', response);
throw new Error('抽奖失败:后端返回数据格式错误');
}
} catch (error) {
console.error('抽奖请求失败:', error);
// 如果后端请求失败,可以回退到前端随机抽奖逻辑
console.log('回退到前端随机抽奖逻辑');
const totalCards = dataManager.getTotalCards();
const leftUsers = dataManager.state.basicData.leftUsers;
let selectedCardIndex = [];
let currentLuckys = [];
let leftCount = leftUsers.length;
// 随机抽取中奖用户和卡片索引
for (let i = 0; i < actualPerCount && leftCount > 0; i++) {
const luckyId = getRandomInt(leftCount);
const selectedUser = leftUsers.splice(luckyId, 1)[0];
// 确保数据格式一致
currentLuckys.push({
jwcode: selectedUser.jwcode || selectedUser[0] || "",
username: selectedUser.username || selectedUser[1] || "",
company: selectedUser.company || selectedUser[2] || "PSST"
});
leftCount--;
let cardIndex = getRandomInt(totalCards);
while (selectedCardIndex.includes(cardIndex)) {
cardIndex = getRandomInt(totalCards);
}
selectedCardIndex.push(cardIndex);
}
console.log('executeLottery - selectedCardIndex:', selectedCardIndex, 'currentLuckys:', currentLuckys);
dataManager.state.currentLuckys = currentLuckys;
// 保存中奖用户到对应奖品
if (!dataManager.state.basicData.luckyUsers[currentPrizeIndex]) {
dataManager.state.basicData.luckyUsers[currentPrizeIndex] = [];
}
dataManager.state.basicData.luckyUsers[currentPrizeIndex].push(...currentLuckys);
// 更新轮次信息
dataManager.updateCurrentRound();
// 展示中奖动画
console.log('executeLottery - calling selectCard');
await renderer3D.selectCard?.(selectedCardIndex, currentLuckys);
console.log('executeLottery - selectCard completed');
}
// 抽奖完成后更新奖品列表数据
try {
await dataManager.updatePrizeList();
console.log('抽奖完成后奖品列表已更新');
} catch (error) {
console.error('更新奖品列表失败:', error);
}
dataManager.setLotteryStatus(false);
isLotting.value = false;
}
function performLottery() {
// 已合并到 executeLottery
}
function changePrize() {
// 可根据中奖情况切换奖品
dataManager.updateCurrentPrize();
}
async function resetLottery() {
if (isLotting.value) return;
dataManager.resetAllData();
renderer3D.addHighlight && renderer3D.addHighlight();
lotteryStore.setLotteryState('idle'); // 直接用
await renderer3D.resetCard([]);
await dataManager.resetData();
// 重置轮次
dataManager.state.currentRound = 1;
renderer3D.switchScreen && renderer3D.switchScreen('enter');
}
return {
isLotting,
executeLottery,
performLottery,
changePrize,
resetLottery
};
}

2
src/views/homePage.vue

@ -2,11 +2,13 @@
<div>
<button @click="router.push('/choujiang')">抽奖</button>
<button @click="router.push('/zhongchou')">众筹</button>
<Mascot />
</div>
</template>
<script setup>
import { useRouter } from "vue-router";
// import Mascot from "../choujiang/lottery/Mascot.vue";
const router = useRouter();

22
src/views/zhongchou/index.vue

@ -306,7 +306,17 @@ const updateProgressDisplay = () => {
// hkFill.style.height = `${hkProgressHeight.value}%`
// }
}
const getQueryVariable = (variable) => {
const query = window.location.search.substring(1)
const vars = query.split('&')
for (let i = 0; i < vars.length; i++) {
const pair = vars[i].split('=')
if (pair[0] === variable) {
return pair[1]
}
}
return ''
}
//
onMounted(() => {
nextTick(() => {
@ -314,11 +324,13 @@ onMounted(() => {
fetchActivity()
updateProgressBarHeight()
updateProgressDisplay()
let token = '';
token = getQueryVariable('token')
localStorage.setItem('localToken', token);
//
window.addEventListener('resize', () => {
nextTick(() => {
updateProgressBarHeight()
updateProgressBarHeight()
})
})
})
@ -1166,7 +1178,7 @@ const hideRules = () => {
/* 调整手机版高度 */
right: 5px;
bottom: -210px;
background-image: url('@/assets/img/zhongchou/手机周年庆装饰.png');
background-image: url('@/assets/img/zhongchou/手机周年庆装饰.png');
/* 需要准备此图片资源 */
}
@ -1181,7 +1193,7 @@ const hideRules = () => {
font-size: 0.9rem;
padding: 6px 12px;
max-width: 85vw;
/* 在极小屏幕上进一步缩小 */
}
}

42
vite.config.js

@ -3,9 +3,15 @@ import vue from '@vitejs/plugin-vue'
import path from 'path'
// https://vite.dev/config/
// export default defineConfig({
// plugins: [vue()],
// })
export default defineConfig({
plugins: [vue()],
resolve: {
plugins: [
vue(),
],
resolve: {
alias: {
'@': path.resolve(process.cwd(), 'src')
}
@ -13,12 +19,30 @@ export default defineConfig({
server: {
proxy: {
'/Api': {
target: 'https://dbqb.nfdxy.net/devLotApi', // 后端基础地址
target: 'https://dbqb.nfdxy.net/devLotApi',
// target: 'http://localhost:8080',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/Api/, ''), // 移除 /api 前缀
// 或者更精确的重写(根据后端路径调整):
// rewrite: (path) => path.replace(/^\/api\/admin/, '/admin'),
},
},
},
rewrite: (path) => path.replace(/^\/Api/, '')
}
}
}
})
// export default defineConfig({
// plugins: [vue()],
// resolve: {
// alias: {
// '@': path.resolve(process.cwd(), 'src')
// }
// },
// server: {
// proxy: {
// '/Api': {
// target: 'https://dbqb.nfdxy.net/devLotApi', // 后端基础地址
// changeOrigin: true,
// rewrite: (path) => path.replace(/^\/Api/, ''), // 移除 /api 前缀
// // 或者更精确的重写(根据后端路径调整):
// // rewrite: (path) => path.replace(/^\/api\/admin/, '/admin'),
// },
// },
// },
// })

22
vite.config.js.timestamp-1753077096347-78067037e45f7.mjs

@ -0,0 +1,22 @@
// vite.config.js
import { defineConfig } from "file:///E:/hxl/gitFile/activityLink/node_modules/vite/dist/node/index.js";
import vue from "file:///E:/hxl/gitFile/activityLink/node_modules/@vitejs/plugin-vue/dist/index.mjs";
var vite_config_default = defineConfig({
plugins: [
vue()
],
server: {
proxy: {
"/Api": {
target: "https://dbqb.nfdxy.net/devLotApi",
// target: 'http://localhost:8080',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/Api/, "")
}
}
}
});
export {
vite_config_default as default
};
//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsidml0ZS5jb25maWcuanMiXSwKICAic291cmNlc0NvbnRlbnQiOiBbImNvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9kaXJuYW1lID0gXCJFOlxcXFxoeGxcXFxcZ2l0RmlsZVxcXFxhY3Rpdml0eUxpbmtcIjtjb25zdCBfX3ZpdGVfaW5qZWN0ZWRfb3JpZ2luYWxfZmlsZW5hbWUgPSBcIkU6XFxcXGh4bFxcXFxnaXRGaWxlXFxcXGFjdGl2aXR5TGlua1xcXFx2aXRlLmNvbmZpZy5qc1wiO2NvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9pbXBvcnRfbWV0YV91cmwgPSBcImZpbGU6Ly8vRTovaHhsL2dpdEZpbGUvYWN0aXZpdHlMaW5rL3ZpdGUuY29uZmlnLmpzXCI7aW1wb3J0IHsgZGVmaW5lQ29uZmlnIH0gZnJvbSAndml0ZSdcbmltcG9ydCB2dWUgZnJvbSAnQHZpdGVqcy9wbHVnaW4tdnVlJ1xuXG4vLyBodHRwczovL3ZpdGUuZGV2L2NvbmZpZy9cbi8vIGV4cG9ydCBkZWZhdWx0IGRlZmluZUNvbmZpZyh7XG4vLyAgIHBsdWdpbnM6IFt2dWUoKV0sXG4vLyB9KVxuXG5leHBvcnQgZGVmYXVsdCBkZWZpbmVDb25maWcoe1xuICBwbHVnaW5zOiBbXG4gICAgdnVlKCksXG4gIF0sXG4gIHNlcnZlcjoge1xuICAgIHByb3h5OiB7XG4gICAgICAnL0FwaSc6IHtcbiAgICAgICAgdGFyZ2V0OiAnaHR0cHM6Ly9kYnFiLm5mZHh5Lm5ldC9kZXZMb3RBcGknLFxuICAgICAgICAvLyB0YXJnZXQ6ICdodHRwOi8vbG9jYWxob3N0OjgwODAnLFxuICAgICAgICBjaGFuZ2VPcmlnaW46IHRydWUsXG4gICAgICAgIHJld3JpdGU6IChwYXRoKSA9PiBwYXRoLnJlcGxhY2UoL15cXC9BcGkvLCAnJylcbiAgICAgIH1cbiAgICB9XG4gIH1cbn0pXG4iXSwKICAibWFwcGluZ3MiOiAiO0FBQTJRLFNBQVMsb0JBQW9CO0FBQ3hTLE9BQU8sU0FBUztBQU9oQixJQUFPLHNCQUFRLGFBQWE7QUFBQSxFQUMxQixTQUFTO0FBQUEsSUFDUCxJQUFJO0FBQUEsRUFDTjtBQUFBLEVBQ0EsUUFBUTtBQUFBLElBQ04sT0FBTztBQUFBLE1BQ0wsUUFBUTtBQUFBLFFBQ04sUUFBUTtBQUFBO0FBQUEsUUFFUixjQUFjO0FBQUEsUUFDZCxTQUFTLENBQUMsU0FBUyxLQUFLLFFBQVEsVUFBVSxFQUFFO0FBQUEsTUFDOUM7QUFBQSxJQUNGO0FBQUEsRUFDRjtBQUNGLENBQUM7IiwKICAibmFtZXMiOiBbXQp9Cg==

98
修改完成总结.md

@ -0,0 +1,98 @@
# 抽奖逻辑修改完成总结
## 修改完成情况
✅ **已完成所有必要的修改**
## 修改文件清单
### 1. API接口层 (`src/api/API.js`)
- ✅ 新增 `drawLottery` 接口
- ✅ 支持传递奖项信息和轮次参数
### 2. 抽奖引擎 (`src/views/choujiang/lottery/lotteryEngine.js`)
- ✅ 修改 `executeLottery` 函数,每轮抽奖请求后端
- ✅ 添加错误处理机制,支持回退到前端随机抽奖
- ✅ 适配新的数据格式 `{ jwcode: "xxx", username: "xxx" }`
### 3. 数据管理器 (`src/views/choujiang/lottery/dataManager.js`)
- ✅ 使用真实用户数据替代假数据
- ✅ 确保数据格式与后端返回格式兼容
- ✅ 支持新旧两种数据格式
### 4. 3D显示组件 (`src/views/choujiang/lottery/Lottery3D.vue`)
- ✅ 修改 `changeCard` 函数,适配新的数据格式
- ✅ 支持显示 `jwcode``username` 字段
- ✅ 保持向后兼容性
### 5. 主抽奖页面 (`src/views/choujiang/index.vue`)
- ✅ 修改气泡提示逻辑,支持新的数据格式
- ✅ 确保中奖用户信息正确显示
## 核心功能
### 1. 后端抽奖接口
```javascript
// 请求参数
{
gradeName: "一等奖",
prizeName: "iPhone 15",
perWin: 5,
round: 1
}
// 返回数据
{
data: [
{ jwcode: "5412", username: "猪八戒22" },
{ jwcode: "45125", username: "宝玉" }
]
}
```
### 2. 错误处理机制
- 后端请求失败时自动回退到前端随机抽奖
- 确保抽奖功能不中断
- 提供详细的错误日志
### 3. 数据格式兼容性
- 支持新格式:`{ jwcode: "5412", username: "猪八戒22" }`
- 支持旧格式:`["5412", "猪八戒22", "PSST"]`
- 自动识别和适配不同格式
## 工作流程
1. **页面初始化** → 获取奖品列表和用户列表
2. **开始抽奖** → 用户点击抽奖按钮
3. **请求后端** → 发送抽奖请求,包含奖项信息和轮次
4. **获取结果** → 后端返回中奖用户列表
5. **显示动画** → 前端根据返回的中奖用户显示3D抽奖动画
6. **保存结果** → 将中奖用户保存到本地状态
## 测试建议
1. **正常流程测试**:验证完整的抽奖流程
2. **异常处理测试**:模拟后端接口异常
3. **数据格式测试**:验证新旧数据格式兼容性
4. **多轮抽奖测试**:验证轮次管理和奖品切换
5. **性能测试**:验证大量用户数据下的表现
## 部署注意事项
1. **后端接口**:确保实现 `/lottery/draw` 接口
2. **数据格式**:确保返回的数据格式符合要求
3. **错误处理**:建议后端提供详细的错误信息
4. **性能优化**:考虑大量并发抽奖请求的处理
## 后续优化建议
1. **缓存机制**:可以考虑缓存用户数据,减少重复请求
2. **实时更新**:可以考虑WebSocket实时更新中奖结果
3. **数据统计**:可以添加抽奖统计和分析功能
4. **界面优化**:可以根据实际需求优化3D动画效果
---
**修改完成时间**:2024年12月19日
**修改状态**:✅ 已完成
**测试状态**:🔄 待测试

92
抽奖逻辑修改说明.md

@ -0,0 +1,92 @@
# 抽奖逻辑修改说明
## 修改概述
本次修改将抽奖逻辑从前端随机抽奖改为每轮抽奖都请求后端获取中奖数据。
## 主要修改内容
### 1. API接口修改 (`src/api/API.js`)
新增了 `drawLottery` 接口,用于每轮抽奖时请求后端:
```javascript
export function drawLottery(data){
return request({
url: '/lottery/draw',
method: 'post',
data: {
gradeName: data.gradeName, // 奖项名称
prizeName: data.prizeName, // 奖品名称
perWin: data.perWin, // 每轮抽奖人数
round: data.round // 当前轮次
}
})
}
```
### 2. 抽奖引擎修改 (`src/views/choujiang/lottery/lotteryEngine.js`)
- 修改 `executeLottery` 函数,每轮抽奖都请求后端
- 后端返回数据格式:`{ data: [{ jwcode: "5412", username: "猪八戒22" }, ...] }`
- 添加了错误处理机制,如果后端请求失败会回退到前端随机抽奖
### 3. 数据管理器修改 (`src/views/choujiang/lottery/dataManager.js`)
- 使用真实的用户数据替代假数据
- 确保数据格式与后端返回格式兼容
- 用户数据格式:`{ jwcode: "5412", username: "猪八戒22", company: "公司名称" }`
### 4. 3D显示组件修改 (`src/views/choujiang/lottery/Lottery3D.vue`)
- 修改 `changeCard` 函数,适配新的数据格式
- 支持显示 `jwcode``username` 字段
## 后端接口要求
### 抽奖接口 `/lottery/draw`
**请求参数:**
```json
{
"gradeName": "一等奖",
"prizeName": "iPhone 15",
"perWin": 5,
"round": 1
}
```
**返回数据格式:**
```json
{
"data": [
{
"jwcode": "5412",
"username": "猪八戒22"
},
{
"jwcode": "45125",
"username": "宝玉"
}
]
}
```
## 工作流程
1. **初始化**:页面加载时获取奖品列表和用户列表
2. **开始抽奖**:用户点击抽奖按钮
3. **请求后端**:发送抽奖请求到后端,包含奖项信息和轮次
4. **获取结果**:后端返回中奖用户列表
5. **显示动画**:前端根据返回的中奖用户显示3D抽奖动画
6. **保存结果**:将中奖用户保存到本地状态
## 错误处理
如果后端抽奖接口请求失败,系统会自动回退到前端随机抽奖逻辑,确保抽奖功能不会中断。
## 兼容性
修改后的代码保持了与原有数据格式的兼容性,支持新旧两种数据格式:
- 新格式:`{ jwcode: "5412", username: "猪八戒22" }`
- 旧格式:`["5412", "猪八戒22", "PSST"]`

113
测试用例.md

@ -0,0 +1,113 @@
# 抽奖逻辑测试用例
## 测试环境准备
1. 确保后端服务正常运行
2. 确保以下接口可用:
- `/prize/list` - 获取奖品列表
- `/user/list` - 获取用户列表
- `/lottery/draw` - 抽奖接口(新增)
## 测试用例
### 测试用例1:正常抽奖流程
**测试步骤:**
1. 打开抽奖页面
2. 点击"进入抽奖"按钮
3. 点击"开始抽奖"按钮
4. 点击"结束抽奖"按钮
5. 观察抽奖结果
**预期结果:**
- 页面正常加载,显示奖品列表和用户卡片
- 抽奖动画正常播放
- 后端返回中奖用户数据
- 3D卡片正确显示中奖用户信息(jwcode和username)
### 测试用例2:后端接口异常处理
**测试步骤:**
1. 模拟后端抽奖接口返回错误
2. 执行抽奖流程
3. 观察系统行为
**预期结果:**
- 系统自动回退到前端随机抽奖逻辑
- 抽奖功能不中断
- 控制台显示错误日志
### 测试用例3:数据格式兼容性
**测试步骤:**
1. 使用新数据格式:`{ jwcode: "5412", username: "猪八戒22" }`
2. 使用旧数据格式:`["5412", "猪八戒22", "PSST"]`
3. 观察显示效果
**预期结果:**
- 两种数据格式都能正确显示
- 卡片内容包含jwcode和username信息
### 测试用例4:多轮抽奖
**测试步骤:**
1. 完成第一轮抽奖
2. 继续第二轮抽奖
3. 观察轮次信息是否正确传递
**预期结果:**
- 每轮抽奖都请求后端
- 轮次信息正确递增
- 中奖用户不重复
### 测试用例5:奖品切换
**测试步骤:**
1. 完成当前奖品的所有轮次抽奖
2. 观察是否自动切换到下一个奖品
3. 验证新奖品的抽奖逻辑
**预期结果:**
- 奖品自动切换
- 新奖品的抽奖参数正确传递
- 轮次重新开始计算
## 调试信息
在浏览器控制台中查看以下日志:
1. **用户数据加载:**
```
userList {data: Array(5)}
```
2. **抽奖请求:**
```
请求后端抽奖,参数: {gradeName: "一等奖", prizeName: "iPhone 15", perWin: 5, round: 1}
```
3. **后端返回结果:**
```
后端抽奖返回结果: {data: Array(5)}
后端返回的中奖用户: [{jwcode: "5412", username: "猪八戒22"}, ...]
```
4. **卡片选择:**
```
executeLottery - selectedCardIndex: [12, 34, 56, 78, 90]
executeLottery - currentLuckys: [{jwcode: "5412", username: "猪八戒22"}, ...]
```
## 常见问题排查
### 问题1:抽奖接口404错误
**解决方案:** 检查后端是否实现了 `/lottery/draw` 接口
### 问题2:数据格式不匹配
**解决方案:** 检查后端返回的数据格式是否符合 `{data: [{jwcode: "xxx", username: "xxx"}]}`
### 问题3:用户数据为空
**解决方案:** 检查 `/user/list` 接口是否正常返回数据
### 问题4:奖品数据为空
**解决方案:** 检查 `/prize/list` 接口是否正常返回数据
Loading…
Cancel
Save