Browse Source

完成大部分逻辑,先测验仓库命令

songtongtong/feature-20250717104937-众筹
Ethereal 1 month ago
parent
commit
b4d59301aa
  1. 3
      .vscode/extensions.json
  2. 214
      package-lock.json
  3. 1
      package.json
  4. BIN
      src/assets/beijingtu.jpg
  5. BIN
      src/assets/daijiemi.png
  6. BIN
      src/assets/image.png
  7. BIN
      src/assets/lottery
  8. BIN
      src/assets/登录.png
  9. 5
      src/main.js
  10. 16
      src/store/lottery.js
  11. 73
      src/utils/config.js
  12. 165
      src/views/choujiang/index.vue
  13. 93
      src/views/choujiang/lottery/CardItem.vue
  14. 43
      src/views/choujiang/lottery/ControlBar.vue
  15. 503
      src/views/choujiang/lottery/Lottery3D.vue
  16. 50
      src/views/choujiang/lottery/MusicPlayer.vue
  17. 386
      src/views/choujiang/lottery/PrizePanel.vue
  18. 37
      src/views/choujiang/lottery/Qipao.vue
  19. 58
      src/views/choujiang/lottery/UserList.vue
  20. 153
      src/views/choujiang/lottery/dataManager.js
  21. 79
      src/views/choujiang/lottery/lotteryEngine.js

3
.vscode/extensions.json

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

214
package-lock.json

@ -11,6 +11,7 @@
"@vitejs/plugin-vue": "^4.6.2", "@vitejs/plugin-vue": "^4.6.2",
"axios": "^1.10.0", "axios": "^1.10.0",
"element-plus": "^2.10.3", "element-plus": "^2.10.3",
"pinia": "^3.0.3",
"vite": "^4.5.3", "vite": "^4.5.3",
"vue": "^3.5.17", "vue": "^3.5.17",
"vue-router": "^4.5.1" "vue-router": "^4.5.1"
@ -564,6 +565,28 @@
"integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==", "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@vue/devtools-kit": {
"version": "7.7.7",
"resolved": "https://registry.npmmirror.com/@vue/devtools-kit/-/devtools-kit-7.7.7.tgz",
"integrity": "sha512-wgoZtxcTta65cnZ1Q6MbAfePVFxfM+gq0saaeytoph7nEa7yMXoi6sCPy4ufO111B9msnw0VOWjPEFCXuAKRHA==",
"dependencies": {
"@vue/devtools-shared": "^7.7.7",
"birpc": "^2.3.0",
"hookable": "^5.5.3",
"mitt": "^3.0.1",
"perfect-debounce": "^1.0.0",
"speakingurl": "^14.0.1",
"superjson": "^2.2.2"
}
},
"node_modules/@vue/devtools-shared": {
"version": "7.7.7",
"resolved": "https://registry.npmmirror.com/@vue/devtools-shared/-/devtools-shared-7.7.7.tgz",
"integrity": "sha512-+udSj47aRl5aKb0memBvcUG9koarqnxNM5yjuREvqwK6T3ap4mn3Zqqc17QrBFTqSMjr3HK1cvStEZpMDpfdyw==",
"dependencies": {
"rfdc": "^1.4.1"
}
},
"node_modules/@vue/reactivity": { "node_modules/@vue/reactivity": {
"version": "3.5.17", "version": "3.5.17",
"resolved": "https://mirrors.huaweicloud.com/repository/npm/@vue/reactivity/-/reactivity-3.5.17.tgz", "resolved": "https://mirrors.huaweicloud.com/repository/npm/@vue/reactivity/-/reactivity-3.5.17.tgz",
@ -725,6 +748,14 @@
"proxy-from-env": "^1.1.0" "proxy-from-env": "^1.1.0"
} }
}, },
"node_modules/birpc": {
"version": "2.5.0",
"resolved": "https://registry.npmmirror.com/birpc/-/birpc-2.5.0.tgz",
"integrity": "sha512-VSWO/W6nNQdyP520F1mhf+Lc2f8pjGQOtoHHm7Ze8Go1kX7akpVIrtTa0fn+HB0QJEDVacl6aO08YE0PgXfdnQ==",
"funding": {
"url": "https://github.com/sponsors/antfu"
}
},
"node_modules/call-bind-apply-helpers": { "node_modules/call-bind-apply-helpers": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://mirrors.huaweicloud.com/repository/npm/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", "resolved": "https://mirrors.huaweicloud.com/repository/npm/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
@ -750,6 +781,20 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/copy-anything": {
"version": "3.0.5",
"resolved": "https://registry.npmmirror.com/copy-anything/-/copy-anything-3.0.5.tgz",
"integrity": "sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==",
"dependencies": {
"is-what": "^4.1.8"
},
"engines": {
"node": ">=12.13"
},
"funding": {
"url": "https://github.com/sponsors/mesqueeb"
}
},
"node_modules/csstype": { "node_modules/csstype": {
"version": "3.1.3", "version": "3.1.3",
"resolved": "https://mirrors.huaweicloud.com/repository/npm/csstype/-/csstype-3.1.3.tgz", "resolved": "https://mirrors.huaweicloud.com/repository/npm/csstype/-/csstype-3.1.3.tgz",
@ -1064,6 +1109,22 @@
"node": ">= 0.4" "node": ">= 0.4"
} }
}, },
"node_modules/hookable": {
"version": "5.5.3",
"resolved": "https://registry.npmmirror.com/hookable/-/hookable-5.5.3.tgz",
"integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ=="
},
"node_modules/is-what": {
"version": "4.1.16",
"resolved": "https://registry.npmmirror.com/is-what/-/is-what-4.1.16.tgz",
"integrity": "sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==",
"engines": {
"node": ">=12.13"
},
"funding": {
"url": "https://github.com/sponsors/mesqueeb"
}
},
"node_modules/lodash": { "node_modules/lodash": {
"version": "4.17.21", "version": "4.17.21",
"resolved": "https://mirrors.huaweicloud.com/repository/npm/lodash/-/lodash-4.17.21.tgz", "resolved": "https://mirrors.huaweicloud.com/repository/npm/lodash/-/lodash-4.17.21.tgz",
@ -1132,6 +1193,11 @@
"node": ">= 0.6" "node": ">= 0.6"
} }
}, },
"node_modules/mitt": {
"version": "3.0.1",
"resolved": "https://registry.npmmirror.com/mitt/-/mitt-3.0.1.tgz",
"integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw=="
},
"node_modules/nanoid": { "node_modules/nanoid": {
"version": "3.3.11", "version": "3.3.11",
"resolved": "https://mirrors.huaweicloud.com/repository/npm/nanoid/-/nanoid-3.3.11.tgz", "resolved": "https://mirrors.huaweicloud.com/repository/npm/nanoid/-/nanoid-3.3.11.tgz",
@ -1156,12 +1222,45 @@
"integrity": "sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw==", "integrity": "sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw==",
"license": "BSD-3-Clause" "license": "BSD-3-Clause"
}, },
"node_modules/perfect-debounce": {
"version": "1.0.0",
"resolved": "https://registry.npmmirror.com/perfect-debounce/-/perfect-debounce-1.0.0.tgz",
"integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA=="
},
"node_modules/picocolors": { "node_modules/picocolors": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://mirrors.huaweicloud.com/repository/npm/picocolors/-/picocolors-1.1.1.tgz", "resolved": "https://mirrors.huaweicloud.com/repository/npm/picocolors/-/picocolors-1.1.1.tgz",
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
"license": "ISC" "license": "ISC"
}, },
"node_modules/pinia": {
"version": "3.0.3",
"resolved": "https://registry.npmmirror.com/pinia/-/pinia-3.0.3.tgz",
"integrity": "sha512-ttXO/InUULUXkMHpTdp9Fj4hLpD/2AoJdmAbAeW2yu1iy1k+pkFekQXw5VpC0/5p51IOR/jDaDRfRWRnMMsGOA==",
"dependencies": {
"@vue/devtools-api": "^7.7.2"
},
"funding": {
"url": "https://github.com/sponsors/posva"
},
"peerDependencies": {
"typescript": ">=4.4.4",
"vue": "^2.7.0 || ^3.5.11"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/pinia/node_modules/@vue/devtools-api": {
"version": "7.7.7",
"resolved": "https://registry.npmmirror.com/@vue/devtools-api/-/devtools-api-7.7.7.tgz",
"integrity": "sha512-lwOnNBH2e7x1fIIbVT7yF5D+YWhqELm55/4ZKf45R9T8r9dE2AIOy8HKjfqzGsoTHFbWbr337O4E0A0QADnjBg==",
"dependencies": {
"@vue/devtools-kit": "^7.7.7"
}
},
"node_modules/postcss": { "node_modules/postcss": {
"version": "8.5.6", "version": "8.5.6",
"resolved": "https://mirrors.huaweicloud.com/repository/npm/postcss/-/postcss-8.5.6.tgz", "resolved": "https://mirrors.huaweicloud.com/repository/npm/postcss/-/postcss-8.5.6.tgz",
@ -1196,6 +1295,11 @@
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/rfdc": {
"version": "1.4.1",
"resolved": "https://registry.npmmirror.com/rfdc/-/rfdc-1.4.1.tgz",
"integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA=="
},
"node_modules/rollup": { "node_modules/rollup": {
"version": "3.29.5", "version": "3.29.5",
"resolved": "https://mirrors.huaweicloud.com/repository/npm/rollup/-/rollup-3.29.5.tgz", "resolved": "https://mirrors.huaweicloud.com/repository/npm/rollup/-/rollup-3.29.5.tgz",
@ -1221,6 +1325,25 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/speakingurl": {
"version": "14.0.1",
"resolved": "https://registry.npmmirror.com/speakingurl/-/speakingurl-14.0.1.tgz",
"integrity": "sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/superjson": {
"version": "2.2.2",
"resolved": "https://registry.npmmirror.com/superjson/-/superjson-2.2.2.tgz",
"integrity": "sha512-5JRxVqC8I8NuOUjzBbvVJAKNM8qoVuH0O77h4WInc/qC2q5IreqKxYwgkga3PfA22OayK2ikceb/B26dztPl+Q==",
"dependencies": {
"copy-anything": "^3.0.2"
},
"engines": {
"node": ">=16"
}
},
"node_modules/vite": { "node_modules/vite": {
"version": "4.5.3", "version": "4.5.3",
"resolved": "https://mirrors.huaweicloud.com/repository/npm/vite/-/vite-4.5.3.tgz", "resolved": "https://mirrors.huaweicloud.com/repository/npm/vite/-/vite-4.5.3.tgz",
@ -1591,6 +1714,28 @@
"resolved": "https://mirrors.huaweicloud.com/repository/npm/@vue/devtools-api/-/devtools-api-6.6.4.tgz", "resolved": "https://mirrors.huaweicloud.com/repository/npm/@vue/devtools-api/-/devtools-api-6.6.4.tgz",
"integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==" "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g=="
}, },
"@vue/devtools-kit": {
"version": "7.7.7",
"resolved": "https://registry.npmmirror.com/@vue/devtools-kit/-/devtools-kit-7.7.7.tgz",
"integrity": "sha512-wgoZtxcTta65cnZ1Q6MbAfePVFxfM+gq0saaeytoph7nEa7yMXoi6sCPy4ufO111B9msnw0VOWjPEFCXuAKRHA==",
"requires": {
"@vue/devtools-shared": "^7.7.7",
"birpc": "^2.3.0",
"hookable": "^5.5.3",
"mitt": "^3.0.1",
"perfect-debounce": "^1.0.0",
"speakingurl": "^14.0.1",
"superjson": "^2.2.2"
}
},
"@vue/devtools-shared": {
"version": "7.7.7",
"resolved": "https://registry.npmmirror.com/@vue/devtools-shared/-/devtools-shared-7.7.7.tgz",
"integrity": "sha512-+udSj47aRl5aKb0memBvcUG9koarqnxNM5yjuREvqwK6T3ap4mn3Zqqc17QrBFTqSMjr3HK1cvStEZpMDpfdyw==",
"requires": {
"rfdc": "^1.4.1"
}
},
"@vue/reactivity": { "@vue/reactivity": {
"version": "3.5.17", "version": "3.5.17",
"resolved": "https://mirrors.huaweicloud.com/repository/npm/@vue/reactivity/-/reactivity-3.5.17.tgz", "resolved": "https://mirrors.huaweicloud.com/repository/npm/@vue/reactivity/-/reactivity-3.5.17.tgz",
@ -1693,6 +1838,11 @@
"proxy-from-env": "^1.1.0" "proxy-from-env": "^1.1.0"
} }
}, },
"birpc": {
"version": "2.5.0",
"resolved": "https://registry.npmmirror.com/birpc/-/birpc-2.5.0.tgz",
"integrity": "sha512-VSWO/W6nNQdyP520F1mhf+Lc2f8pjGQOtoHHm7Ze8Go1kX7akpVIrtTa0fn+HB0QJEDVacl6aO08YE0PgXfdnQ=="
},
"call-bind-apply-helpers": { "call-bind-apply-helpers": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://mirrors.huaweicloud.com/repository/npm/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", "resolved": "https://mirrors.huaweicloud.com/repository/npm/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
@ -1710,6 +1860,14 @@
"delayed-stream": "~1.0.0" "delayed-stream": "~1.0.0"
} }
}, },
"copy-anything": {
"version": "3.0.5",
"resolved": "https://registry.npmmirror.com/copy-anything/-/copy-anything-3.0.5.tgz",
"integrity": "sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==",
"requires": {
"is-what": "^4.1.8"
}
},
"csstype": { "csstype": {
"version": "3.1.3", "version": "3.1.3",
"resolved": "https://mirrors.huaweicloud.com/repository/npm/csstype/-/csstype-3.1.3.tgz", "resolved": "https://mirrors.huaweicloud.com/repository/npm/csstype/-/csstype-3.1.3.tgz",
@ -1910,6 +2068,16 @@
"function-bind": "^1.1.2" "function-bind": "^1.1.2"
} }
}, },
"hookable": {
"version": "5.5.3",
"resolved": "https://registry.npmmirror.com/hookable/-/hookable-5.5.3.tgz",
"integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ=="
},
"is-what": {
"version": "4.1.16",
"resolved": "https://registry.npmmirror.com/is-what/-/is-what-4.1.16.tgz",
"integrity": "sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A=="
},
"lodash": { "lodash": {
"version": "4.17.21", "version": "4.17.21",
"resolved": "https://mirrors.huaweicloud.com/repository/npm/lodash/-/lodash-4.17.21.tgz", "resolved": "https://mirrors.huaweicloud.com/repository/npm/lodash/-/lodash-4.17.21.tgz",
@ -1957,6 +2125,11 @@
"mime-db": "1.52.0" "mime-db": "1.52.0"
} }
}, },
"mitt": {
"version": "3.0.1",
"resolved": "https://registry.npmmirror.com/mitt/-/mitt-3.0.1.tgz",
"integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw=="
},
"nanoid": { "nanoid": {
"version": "3.3.11", "version": "3.3.11",
"resolved": "https://mirrors.huaweicloud.com/repository/npm/nanoid/-/nanoid-3.3.11.tgz", "resolved": "https://mirrors.huaweicloud.com/repository/npm/nanoid/-/nanoid-3.3.11.tgz",
@ -1967,11 +2140,34 @@
"resolved": "https://mirrors.huaweicloud.com/repository/npm/normalize-wheel-es/-/normalize-wheel-es-1.2.0.tgz", "resolved": "https://mirrors.huaweicloud.com/repository/npm/normalize-wheel-es/-/normalize-wheel-es-1.2.0.tgz",
"integrity": "sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw==" "integrity": "sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw=="
}, },
"perfect-debounce": {
"version": "1.0.0",
"resolved": "https://registry.npmmirror.com/perfect-debounce/-/perfect-debounce-1.0.0.tgz",
"integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA=="
},
"picocolors": { "picocolors": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://mirrors.huaweicloud.com/repository/npm/picocolors/-/picocolors-1.1.1.tgz", "resolved": "https://mirrors.huaweicloud.com/repository/npm/picocolors/-/picocolors-1.1.1.tgz",
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="
}, },
"pinia": {
"version": "3.0.3",
"resolved": "https://registry.npmmirror.com/pinia/-/pinia-3.0.3.tgz",
"integrity": "sha512-ttXO/InUULUXkMHpTdp9Fj4hLpD/2AoJdmAbAeW2yu1iy1k+pkFekQXw5VpC0/5p51IOR/jDaDRfRWRnMMsGOA==",
"requires": {
"@vue/devtools-api": "^7.7.2"
},
"dependencies": {
"@vue/devtools-api": {
"version": "7.7.7",
"resolved": "https://registry.npmmirror.com/@vue/devtools-api/-/devtools-api-7.7.7.tgz",
"integrity": "sha512-lwOnNBH2e7x1fIIbVT7yF5D+YWhqELm55/4ZKf45R9T8r9dE2AIOy8HKjfqzGsoTHFbWbr337O4E0A0QADnjBg==",
"requires": {
"@vue/devtools-kit": "^7.7.7"
}
}
}
},
"postcss": { "postcss": {
"version": "8.5.6", "version": "8.5.6",
"resolved": "https://mirrors.huaweicloud.com/repository/npm/postcss/-/postcss-8.5.6.tgz", "resolved": "https://mirrors.huaweicloud.com/repository/npm/postcss/-/postcss-8.5.6.tgz",
@ -1987,6 +2183,11 @@
"resolved": "https://mirrors.huaweicloud.com/repository/npm/proxy-from-env/-/proxy-from-env-1.1.0.tgz", "resolved": "https://mirrors.huaweicloud.com/repository/npm/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
}, },
"rfdc": {
"version": "1.4.1",
"resolved": "https://registry.npmmirror.com/rfdc/-/rfdc-1.4.1.tgz",
"integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA=="
},
"rollup": { "rollup": {
"version": "3.29.5", "version": "3.29.5",
"resolved": "https://mirrors.huaweicloud.com/repository/npm/rollup/-/rollup-3.29.5.tgz", "resolved": "https://mirrors.huaweicloud.com/repository/npm/rollup/-/rollup-3.29.5.tgz",
@ -2000,6 +2201,19 @@
"resolved": "https://mirrors.huaweicloud.com/repository/npm/source-map-js/-/source-map-js-1.2.1.tgz", "resolved": "https://mirrors.huaweicloud.com/repository/npm/source-map-js/-/source-map-js-1.2.1.tgz",
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==" "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="
}, },
"speakingurl": {
"version": "14.0.1",
"resolved": "https://registry.npmmirror.com/speakingurl/-/speakingurl-14.0.1.tgz",
"integrity": "sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ=="
},
"superjson": {
"version": "2.2.2",
"resolved": "https://registry.npmmirror.com/superjson/-/superjson-2.2.2.tgz",
"integrity": "sha512-5JRxVqC8I8NuOUjzBbvVJAKNM8qoVuH0O77h4WInc/qC2q5IreqKxYwgkga3PfA22OayK2ikceb/B26dztPl+Q==",
"requires": {
"copy-anything": "^3.0.2"
}
},
"vite": { "vite": {
"version": "4.5.3", "version": "4.5.3",
"resolved": "https://mirrors.huaweicloud.com/repository/npm/vite/-/vite-4.5.3.tgz", "resolved": "https://mirrors.huaweicloud.com/repository/npm/vite/-/vite-4.5.3.tgz",

1
package.json

@ -12,6 +12,7 @@
"@vitejs/plugin-vue": "^4.6.2", "@vitejs/plugin-vue": "^4.6.2",
"axios": "^1.10.0", "axios": "^1.10.0",
"element-plus": "^2.10.3", "element-plus": "^2.10.3",
"pinia": "^3.0.3",
"vite": "^4.5.3", "vite": "^4.5.3",
"vue": "^3.5.17", "vue": "^3.5.17",
"vue-router": "^4.5.1" "vue-router": "^4.5.1"

BIN
src/assets/beijingtu.jpg

After

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

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/lottery

BIN
src/assets/登录.png

After

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

5
src/main.js

@ -1,6 +1,9 @@
import { createApp } from 'vue' import { createApp } from 'vue'
import { createPinia } from 'pinia'
import './style.css' import './style.css'
import App from './App.vue' import App from './App.vue'
import router from './router' import router from './router'
// import { createPinia } from 'pinia'
createApp(App).use(router).mount('#app')
createApp(App).use(router).use(createPinia()).mount('#app')

16
src/store/lottery.js

@ -0,0 +1,16 @@
// 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
function setLotteryState(state) {
lotteryState.value = state
}
return {
lotteryState,
setLotteryState
}
})

73
src/utils/config.js

@ -0,0 +1,73 @@
// 【2,3】:第3列,第4行
export const NUMBER_MATRIX = {
"0": [
[1, 1], [4, 1], [6, 1], [10, 1], [11, 1], [12, 1], [13, 1],[15,1],[16,1],[17,1],[18,1],
[1,2], [4,2], [6,2], [13,2], [18,2],
[1,3],[2,3],[3,3],[4,3] ,[6,3], [12,3], [17,3],
[1,4], [4,4], [6,4], [11,4], [16,4],
[1,5],[4,5], [6,5],[7,5],[8,5], [10,5],[11,5],[12,5],[13,5], [16,5]
],
"1": [
[1, 0], [2, 0],
[0, 1], [1, 1], [2, 1],
[1, 2],
[1, 3],
[1, 4]
],
"2": [
[0, 0], [1, 0], [2, 0], [3, 0],
[3, 1],
[0, 2], [1, 2], [2, 2], [3, 2],
[0, 3],
[0, 4], [1, 4], [2, 4], [3, 4]
],
"3": [
[0, 0], [1, 0], [2, 0], [3, 0],
[3, 1],
[0, 2], [1, 2], [2, 2], [3, 2],
[3, 3],
[0, 4], [1, 4], [2, 4], [3, 4]
],
"4": [
[0, 0], [3, 0],
[0, 1], [3, 1],
[0, 2], [1, 2], [2, 2], [3, 2],
[3, 3],
[3, 4]
],
"5": [
[0, 0], [1, 0], [2, 0], [3, 0],
[0, 1],
[0, 2], [1, 2], [2, 2], [3, 2],
[3, 3],
[0, 4], [1, 4], [2, 4], [3, 4]
],
"6": [
[0, 0], [1, 0], [2, 0], [3, 0],
[0, 1],
[0, 2], [1, 2], [2, 2], [3, 2],
[0, 3], [3, 3],
[0, 4], [1, 4], [2, 4], [3, 4]
],
"7": [
[0, 0], [1, 0], [2, 0], [3, 0],
[3, 1],
[2, 2],
[1, 3],
[0, 4]
],
"8": [
[0, 0], [1, 0], [2, 0], [3, 0],
[0, 1], [3, 1],
[0, 2], [1, 2], [2, 2], [3, 2],
[0, 3], [3, 3],
[0, 4], [1, 4], [2, 4], [3, 4]
],
"9": [
[0, 0], [1, 0], [2, 0], [3, 0],
[0, 1], [3, 1],
[0, 2], [1, 2], [2, 2], [3, 2],
[3, 3],
[0, 4], [1, 4], [2, 4], [3, 4]
]
};

165
src/views/choujiang/index.vue

@ -1,13 +1,172 @@
<template> <template>
<div>
抽奖
</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 />
<!-- <UserList
:lucky-users="
dataManager.state.basicData.luckyUsers[
dataManager.state.currentPrize?.type
] || []
"
:left-users="dataManager.state.basicData.leftUsers"
/> -->
<Qipao :text="qipaoText" :show="showQipao" />
</div>
</template> </template>
<script setup> <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 { ref, onMounted, nextTick, computed, watch } from "vue";
import { useDataManager } from "./lottery/dataManager.js";
import { useLotteryEngine } from "./lottery/lotteryEngine.js";
import { useLotteryStore } from "../../store/lottery"; //
const qipaoText = ref("");
const showQipao = 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 isDisabled = ref(false);
watch(isDisabled, (newVal, oldVal) => {
console.log("isDisabled 变化:", oldVal, "->", newVal);
});
//
const dataManager = useDataManager();
let lottery3DRef = 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 () => {
await dataManager.getBasicData();
await dataManager.getUsers();
});
function showLotteryQipao() {
const luckys = dataManager.state.currentLuckys;
const prize = dataManager.state.currentPrize;
if (!luckys || luckys.length === 0) return;
const names = luckys.map((item) => item[1]).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":
//
console.log("lotteryState 变更前:", lotteryState.value, "-> ready");
lotteryState.value = "ready";
console.log("lotteryState 变更后:", lotteryState.value);
await lottery3DRef.value?.switchScreen?.("lottery");
break;
case "ready":
//
console.log("lotteryState 变更前:", lotteryState.value, "-> rotating");
lotteryState.value = "rotating";
console.log("lotteryState 变更后:", lotteryState.value);
//
// isRunning.value = false; //
await lottery3DRef.value?.rotateBallStart?.();
break;
case "rotating":
//
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":
// result
await lottery3DRef.value?.switchScreen?.("lottery");
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> </script>
<style scoped> <style scoped>
.choujiang-main {
width: 100vw;
height: 100vh;
position: relative;
overflow: hidden;
/* 添加背景图片 */
background: url('../../assets/登录.png') ;
background-size: 1920px 980px;
}
</style> </style>

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

@ -0,0 +1,93 @@
<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">{{ (user[0] || '') + '\n' + (user[2] || '') }}</div>
</div>
</template>
<script setup>
import { computed } 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 cardStyle = computed(() => {
if (props.isBold && props.showTable) {
if (lotteryState.value === 'idle') {
return {
backgroundColor:'rgba(226, 60, 38, 1)',
// background: 'linear-gradient(135deg, rgba(243,153,38,0.7) 0%, rgba(207,56,35,1) 100%)',
width: '130px',
height: '170px',
// width: '68px',
// height: '88px',
};
}
// return {
// backgroundColor: 'rgb(226, 64, 35)',
// // backgroundColor: 'rgb(226, 164, 35)',
// width: '125px',
// height: '170px',
// };
}
return {
// background: 'linear-gradient(135deg,rgba(255, 170, 22, 100) 0%, rgba(255, 170, 22, 100) 100%)',
// backgroundColor:'rgba(255, 170, 22, 100)',
backgroundColor:'rgba(254, 177, 48, 100)',
width: '130px',
height: '170px',
// width: '68px',
// height: '88px',
border: '1px solid rgb(255,255,255)',
};
});
</script>
<style scoped>
.element {
transition: background-Color 2s;
/* 你的基础样式 */
}
.lightitem {
/* 高亮样式 */
}
.highlight {
/* 响应式高亮样式 */
}
.prize {
/* 中奖样式 */
}
.company { /* ... */ }
.name { /* ... */ }
.details {
font-size: 30px;
text-align: center;
display: flex;
flex-direction: column;
justify-content: center;
height: 100%;
}
</style>

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

@ -0,0 +1,43 @@
<template>
<div class="control-bar">
<button :disabled="isDisabled"
@click="$emit('lottery-click')">
{{ 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;
}
button {
padding: 10px 24px;
font-size: 18px;
border-radius: 6px;
border: none;
background: #0078ff;
color: #fff;
cursor: pointer;
transition: background 0.2s;
}
button:hover {
background: #005bb5;
}
</style>

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

@ -0,0 +1,503 @@
<template>
<div class="lottery-3d-container">
<div ref="threeContainer" class="three-container"></div>
</div>
</template>
<script setup>
import { ref, onMounted, onBeforeUnmount, defineExpose, watch } 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";
const threeContainer = ref(null);
let renderer, scene, camera, animationId;
// let controls; // controls
// 3D
const threeDCards = [];
const targets = {
table: [],
sphere: [],
};
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 -= 400; // 10px// object.position.y -= 20; // yOffset
vector.copy(object.position).multiplyScalar(2);
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;
let tag = -(currentLuckys.length - 1) / 2;
const locates = [];
// , 5
if (currentLuckys.length > 5) {
const yPosition = [-87, 87];
const l = selectedCardIndex.length;
const mid = Math.ceil(l / 2);
tag = -(mid - 1) / 2;
for (let i = 0; i < mid; i++) {
locates.push({
x: tag * width,
y: yPosition[0],
});
tag++;
}
tag = -(l - mid - 1) / 2;
for (let i = mid; i < l; i++) {
locates.push({
x: tag * width,
y: yPosition[1],
});
tag++;
}
} else {
for (let i = selectedCardIndex.length; i > 0; i--) {
locates.push({
x: tag * width,
y: 0,
});
tag++;
}
}
console.log("locates calculated:", locates);
selectedCardIndex.forEach((cardIndex, index) => {
// console.log("animating card:", cardIndex, "to position:", locates[index]);
changeCard(cardIndex, currentLuckys[index]);
const object = threeDCards[cardIndex];
new TWEEN.Tween(object.position)
.to(
{
x: locates[index].x,
y: locates[index].y,
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");
});
new TWEEN.Tween({})
.to({}, duration * 2)
.onUpdate(() => render())
.start()
.onComplete(() => {
console.log("selectCard animation completed");
resolve();
});
});
}
function resetCard(selectedCardIndex, duration = 500) {
if (!selectedCardIndex || selectedCardIndex.length === 0) {
return Promise.resolve();
}
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];
object.element.classList.remove("prize");
});
resolve();
});
});
}
function changeCard(cardIndex, user) {
const card = threeDCards[cardIndex].element;
card.innerHTML = `<div class="company">${
user.company || ""
}</div><div class="name">${user[1]}</div><div class="details">${
user[0] || ""
}<br/>${user[2] || "PSST"}</div>`;
}
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(() => {
// 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
const highlightCells = NUMBER_MATRIX["0"].map(([x, y]) => `${x}-${y}`);
const config = {
ROW_COUNT: 7, // 5
COLUMN_COUNT: 20, // 4
HIGHLIGHT_CELL: highlightCells,
COMPANY: "演示公司",
};
const member = [
[0, "张三"],
[1, "李四"],
[2, "王五"],
[3, "赵六"],
[4, "孙七"],
[5, "周八"],
[6, "吴九"],
[7, "郑十"],
[8, "钱十一"],
[9, "孙十二"],
[10, "李十三"],
[11, "周十四"],
[12, "吴十五"],
[13, "郑十六"],
[14, "钱十七"],
[15, "孙十八"],
[16, "李十九"],
[17, "周二十"],
[18, "吴二一"],
[19, "郑二二"],
];
const length = member.length;
const showTable = true;
const position = {
x: (100 * config.COLUMN_COUNT - 20) / 2,
y: (160 * config.ROW_COUNT - 20) / 2,
};
createCards(member, length, showTable, position, config); // 3.
createSphereTargets();
//
render();
animate();
//
setTimeout(() => {
switchScreen("enter");
}, 500);
window.addEventListener("resize", onWindowResize);
});
onBeforeUnmount(() => {
window.removeEventListener("resize", onWindowResize);
if (animationId) cancelAnimationFrame(animationId);
if (highlightTimeout) {
clearTimeout(highlightTimeout);
highlightTimeout = 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,
});
</script>
<style scoped>
.lottery-3d-container {
width: 100vw;
height: 100vh;
overflow: hidden;
position: relative;
}
.three-container {
width: 100%;
height: 100%;
}
</style>

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

@ -0,0 +1,50 @@
<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 } from 'vue';
const musicSrc = '/src/assets/lottery/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;
}
}
</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>

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

@ -0,0 +1,386 @@
<template>
<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 }"
@click="handleReveal(idx)"
style="cursor: 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">
{{ 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>
<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
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">
Homily ID
</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 } from "vue";
const props = defineProps({
prizes: Array,
});
//
const revealedCount = ref(0);
//
const lastRevealedIdx = ref(-1);
//
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: "六等奖" },
]);
function openWinnerList() {
showWinnerList.value = true;
}
function closeWinnerList() {
showWinnerList.value = false;
}
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;
modalTop.value = rect.bottom + 4; // 4px
}
});
}
}
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;
}
.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;
}
.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;
bottom: 0;
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
z-index: 20;
/* 移除 move-up 相关 */
}
.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);
}
.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;
padding-top: 12px;
min-width: 280px;
max-width: 90vw;
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.12);
position: relative;
}
.winner-modal-title {
font-size: 22px;
font-weight: bold;
margin-bottom: 18px;
text-align: center;
}
.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;
}
.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;
}
</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>

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

@ -0,0 +1,58 @@
<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>
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>

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

@ -0,0 +1,153 @@
import { reactive } from 'vue';
export function useDataManager() {
const state = reactive({
basicData: {
prizes: [],
users: [],
luckyUsers: {},
leftUsers: []
},
currentPrizeIndex: 0,
currentPrize: null,
currentLuckys: [],
isLotting: false,
config: {
prizes: [],
EACH_COUNT: [],
COMPANY: '',
HIGHLIGHT_CELL: [],
Resolution: 1
}
});
async function getBasicData() {
// 假数据,后续可替换为接口
const fakePrizes = [
{ type: 0, title: '特别奖', text: 'iPad', count: 10, img: '/src/assets/lottery/ipad.jpg' },
{ type: 1, title: '一等奖', text: 'Kindle', count: 20, img: '/src/assets/lottery/kindle.jpg' },
{ type: 2, title: '二等奖', text: 'MacBook Pro', count: 10, img: '/src/assets/lottery/mbp.jpg' }
];
const fakeEachCount = [10, 10, 10];
const fakeCompany = '前端假公司';
const fakeLuckyData = {};
fakePrizes.forEach(prize => {
fakeLuckyData[prize.type] = [];
});
const fakeLeftUsers = [
[0, "张三"],
[1, "李四"],
[2, "王五"],
[3, "赵六"],
[4, "孙七"],
[5, "周八"],
[6, "吴九"],
[7, "郑十"],
[8, "钱十一"],
[9, "孙十二"],
[10, "李十三"],
[11, "周十四"],
[12, "吴十五"],
[13, "郑十六"],
[14, "钱十七"],
[15, "孙十八"],
[16, "李十九"],
[17, "周二十"],
[18, "吴二一"],
[19, "郑二二"],
];
state.config.prizes = fakePrizes;
state.config.EACH_COUNT = fakeEachCount;
state.config.COMPANY = fakeCompany;
state.config.HIGHLIGHT_CELL = [];
state.basicData.prizes = fakePrizes;
state.basicData.leftUsers = fakeLeftUsers.slice();
state.basicData.luckyUsers = fakeLuckyData;
determineCurrentPrize();
return Promise.resolve({
cfgData: {
prizes: fakePrizes,
EACH_COUNT: fakeEachCount,
COMPANY: fakeCompany
},
leftUsers: fakeLeftUsers.slice(),
luckyData: fakeLuckyData
});
}
async function getUsers() {
const fakeUsers = [
[0, "张三"],
[1, "李四"],
[2, "王五"],
[3, "赵六"],
[4, "孙七"],
[5, "周八"],
[6, "吴九"],
[7, "郑十"],
[8, "钱十一"],
[9, "孙十二"],
[10, "李十三"],
[11, "周十四"],
[12, "吴十五"],
[13, "郑十六"],
[14, "钱十七"],
[15, "孙十八"],
[16, "李十九"],
[17, "周二十"],
[18, "吴二一"],
[19, "郑二二"],
];
state.basicData.users = fakeUsers;
return Promise.resolve(fakeUsers);
}
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(); }
return {
state,
getBasicData,
getUsers,
saveData,
setErrorData,
exportData,
resetData,
getTotalCards,
setLotteryStatus,
resetAllData,
updateCurrentPrize
};
}

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

@ -0,0 +1,79 @@
import { ref } from 'vue';
import { useLotteryStore } from '../../../store/lottery' // 路径根据实际情况调整
function getRandomInt(max) {
return Math.floor(Math.random() * max);
}
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 perCount = dataManager.state.config.EACH_COUNT[dataManager.state.currentPrizeIndex] || 1;
const totalCards = renderer3D.getTotalCards ? renderer3D.getTotalCards() : 50;
const leftUsers = dataManager.state.basicData.leftUsers;
let selectedCardIndex = [];
let currentLuckys = [];
let leftCount = leftUsers.length;
console.log('executeLottery - perCount:', perCount, 'leftCount:', leftCount, 'totalCards:', totalCards);
// 随机抽取中奖用户和卡片索引
for (let i = 0; i < perCount && leftCount > 0; i++) {
const luckyId = getRandomInt(leftCount);
currentLuckys.push(leftUsers.splice(luckyId, 1)[0]);
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;
// 展示中奖动画
console.log('executeLottery - calling selectCard');
await renderer3D.selectCard?.(selectedCardIndex, currentLuckys);
console.log('executeLottery - selectCard completed');
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();
renderer3D.switchScreen && renderer3D.switchScreen('enter');
}
return {
isLotting,
executeLottery,
performLottery,
changePrize,
resetLottery
};
}
Loading…
Cancel
Save