Lottery.vue 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397
  1. <template>
  2. <page-layer>
  3. <template #aside>
  4. <div style="width: 300px;height: 100%;
  5. background-size: 100% 100%;
  6. background-repeat: no-repeat;
  7. background-image: url('http://webhis.thyy.cn:8080/resource/image/happyAside.jpg')">
  8. <div style="height: 1px"></div>
  9. <div style="background-color: rgba(255,255,255,0.7); margin: 16px; padding: 2px; border-radius: 8px">
  10. <div style="margin: 12px 0 12px 10px; font-size: 18px; color: #014841; font-weight: bold">
  11. 奖项设置
  12. <span class="reset-pool" @click="initLotteryPool">重置为初始状态</span>
  13. </div>
  14. <div style="margin-left: 12px">
  15. <div v-for="item in lotteryPool" :key="item.code">
  16. <div style="display: flex; line-height: 32px; font-size: 16px">
  17. <div style="width: 65px; color: black; font-weight: bold">{{ item.name }}:</div>
  18. <div style="width: 40px; color: #007bd9; font-weight: bold">{{ item.amount }} 份</div>
  19. <div style="width: calc(100% - 120px); text-align: right">
  20. <el-button circle icon="Edit" @click="editRule(item)"></el-button>
  21. <el-checkbox v-model="item.showWinner" @change="refreshHistory"
  22. style="margin-left: 8px">名单</el-checkbox>
  23. </div>
  24. </div>
  25. </div>
  26. </div>
  27. </div>
  28. <div style="width: 300px; text-align: center; margin-top: 20px">
  29. <div style="color: white;text-align: left;padding-left: 50px;margin-bottom: 12px">
  30. <el-checkbox v-model="continuous">连续抽奖</el-checkbox>
  31. </div>
  32. <el-button style="width: 200px" size="large" :disabled="duringLottery"
  33. type="danger" icon="Star" @click="startLottery">开始抽奖
  34. </el-button>
  35. <el-scrollbar ref="wonUserRef" height="510px" style="margin-top: 20px">
  36. <div v-for="user in historyWonUsers" style="line-height: 30px; font-size: 16px">
  37. <span style="color: white">{{user.codeRs}} - {{user.name}}:</span>
  38. <span style="color: white; font-weight: bold">{{user.wonName}}</span>
  39. </div>
  40. </el-scrollbar>
  41. </div>
  42. </div>
  43. </template>
  44. <template #main>
  45. <div style="position:relative;width: 100%; height: 100%;
  46. overflow-y: hidden;
  47. background-size: 100% 100%;
  48. background-repeat: no-repeat;
  49. background-image: url('http://webhis.thyy.cn:8080/resource/image/happyNewYear.jpg')">
  50. <div style="height: 19%;width: 100%;display: flex;align-items: end;
  51. justify-content: center;color: white;font-size: 56px;font-weight: bold">
  52. 当前奖项:{{ currentLotteryName }}
  53. </div>
  54. <div style="height: 60%; width: 100%; display: flex;align-items: center;
  55. justify-content: center;color: white;font-size: 152px;font-weight: bold">
  56. <span id="winner-div">{{ currentUserName }}</span>
  57. </div>
  58. <div style="position: absolute; top:0;right:0;bottom: 0">
  59. <canvas id="canvas"></canvas>
  60. </div>
  61. </div>
  62. </template>
  63. </page-layer>
  64. </template>
  65. <script setup>
  66. import PageLayer from "@/layout/PageLayer.vue";
  67. import {ElMessageBox} from "element-plus";
  68. import {selectLotteryUsers, chooseWinner, recordLotteryResult, selectWonUsers} from "@/api/single-page/lottery";
  69. const continuous = ref(false)
  70. const lotteryPool = ref([])
  71. const lotteryUsers = ref([])
  72. const duringLottery = ref(false)
  73. const winner = reactive({
  74. index: -1,
  75. lotteryCode: '',
  76. userCodeRs: '',
  77. userName: ''
  78. })
  79. const currentLotteryName = ref('三等奖')
  80. const currentUserName = ref('等待抽奖')
  81. function initLotteryPool() {
  82. lotteryPool.value = [
  83. {code: 0, name: '特等奖', amount: 0, round: 0, showWinner: false},
  84. {code: 1, name: '一等奖', amount: 0, round: 0, showWinner: false},
  85. {code: 2, name: '二等奖', amount: 0, round: 0, showWinner: false},
  86. {code: 3, name: '三等奖', amount: 0, round: 0, showWinner: false},
  87. ]
  88. localStorage.setItem('lotteryPool', JSON.stringify(lotteryPool.value))
  89. }
  90. let carouselInterval = null
  91. function moveToWinner() {
  92. let offset = continuous.value ? 1000 : 4500;
  93. let timer = 0;
  94. carouselInterval = setInterval(() => {
  95. timer += 100
  96. if (timer >= offset) {
  97. winnerExposed()
  98. return
  99. }
  100. let index = Math.floor(Math.random() * lotteryUsers.value.length);
  101. let tempUser = lotteryUsers.value[index]
  102. currentUserName.value = tempUser.codeRs + ' - ' + tempUser.name
  103. if (tempUser.codeRs === winner.userCodeRs && timer >= offset) {
  104. winnerExposed()
  105. }
  106. }, 100)
  107. }
  108. function winnerExposed() {
  109. recordLotteryResult(winner).then(() => {
  110. currentUserName.value = winner.userCodeRs + ' - ' + winner.userName
  111. duringLottery.value = false;
  112. clearInterval(carouselInterval)
  113. refreshHistory()
  114. startFirework()
  115. const element = document.getElementById('winner-div')
  116. element.style.transitionDuration = '.5s'
  117. element.style.transitionTimingFunction = 'ease-in-out'
  118. element.style.transformOrigin = 'center center'
  119. element.style.transform = `scale(1.3, 1.3)`;
  120. // 完成后重置样式
  121. setTimeout(() => {
  122. element.style.removeProperty('transition-duration');
  123. element.style.removeProperty('transition-timing-function');
  124. element.style.removeProperty('transform-origin');
  125. element.style.removeProperty('transform');
  126. if (continuous.value) {
  127. setTimeout(() => {
  128. startLottery()
  129. }, 400);
  130. }
  131. }, 600);
  132. })
  133. }
  134. function startLottery() {
  135. duringLottery.value = true
  136. getWinnableLottery().then(item => {
  137. chooseWinner(item).then(res => {
  138. winner.lotteryCode = item.code
  139. winner.userCodeRs = res.codeRs
  140. winner.userName = res.name
  141. moveToWinner()
  142. })
  143. }).catch(() => {
  144. duringLottery.value = false
  145. })
  146. }
  147. function getWinnableLottery() {
  148. return new Promise((resolve, reject) => {
  149. for (let i = lotteryPool.value.length - 1; i >= 0; i--) {
  150. const item = lotteryPool.value[i]
  151. if (item.amount === 1) {
  152. continuous.value = false
  153. }
  154. if (item.amount > 0) {
  155. item.amount -= 1
  156. item.round += 1
  157. item.showWinner = true
  158. currentLotteryName.value = item.name
  159. localStorage.setItem('lotteryPool', JSON.stringify(lotteryPool.value))
  160. resolve(item)
  161. return
  162. } else {
  163. item.showWinner = false
  164. }
  165. }
  166. reject()
  167. })
  168. }
  169. function editRule(item) {
  170. ElMessageBox.prompt(`设置${item.name}数量:`, '提示', {
  171. type: "warning",
  172. confirmButtonText: '确定',
  173. cancelButtonText: '取消',
  174. inputPattern: /^100$|^(\d|[1-9]\d)$/,
  175. inputErrorMessage: '请输入 0至100 的整数',
  176. }).then(({value}) => {
  177. item.amount = Number.parseInt(value)
  178. localStorage.setItem('lotteryPool', JSON.stringify(lotteryPool.value))
  179. })
  180. }
  181. const historyWonUsers = ref([])
  182. function refreshHistory () {
  183. const params = []
  184. lotteryPool.value.forEach(item => {
  185. if (item.showWinner) {
  186. params.push(item.code)
  187. }
  188. })
  189. selectWonUsers(params).then(res => {
  190. historyWonUsers.value = res
  191. if (res.length > 17) {
  192. scrollIfExceedLength(res.length - 17)
  193. }
  194. })
  195. }
  196. const wonUserRef = ref(null)
  197. let scrollInterval = null
  198. let target = 0;
  199. function scrollIfExceedLength(len) {
  200. let height = len * 30;
  201. clearInterval(scrollInterval)
  202. nextTick(() => {
  203. const wrap = wonUserRef.value.wrapRef;
  204. scrollInterval = setInterval(() => {
  205. target += 1
  206. if (target > (height + 15)) {
  207. target = -1
  208. } else {
  209. wrap.scrollTop = target
  210. }
  211. }, 50)
  212. })
  213. }
  214. onDeactivated(() => {
  215. clearInterval(scrollInterval)
  216. })
  217. onMounted(() => {
  218. let storage = localStorage.getItem('lotteryPool')
  219. if (storage) {
  220. lotteryPool.value = JSON.parse(storage);
  221. } else {
  222. initLotteryPool()
  223. }
  224. selectLotteryUsers().then(res => {
  225. lotteryUsers.value = res
  226. initFireworkCanvas()
  227. refreshHistory()
  228. })
  229. })
  230. let canvas, canvasContext, w, h, particles = [], probability = 0.04,
  231. xPoint, yPoint;
  232. const fireworkRunnable = ref(false)
  233. function initFireworkCanvas() {
  234. canvas = document.getElementById("canvas");
  235. canvasContext = canvas.getContext("2d");
  236. resizeCanvas();
  237. }
  238. function resizeCanvas() {
  239. if (canvas) {
  240. w = canvas.width = window.innerWidth;
  241. h = canvas.height = window.innerHeight;
  242. }
  243. }
  244. function startFirework() {
  245. fireworkRunnable.value = true
  246. window.requestAnimationFrame(updateWorld);
  247. setTimeout(() => {
  248. fireworkRunnable.value = false
  249. canvasContext.clearRect(0, 0, canvas.width, canvas.height);
  250. }, 4000)
  251. }
  252. function updateWorld() {
  253. if (fireworkRunnable.value) {
  254. update();
  255. paint();
  256. window.requestAnimationFrame(updateWorld);
  257. }
  258. }
  259. function update() {
  260. if (particles.length < 500 && Math.random() < probability) {
  261. createFirework();
  262. }
  263. let alive = [];
  264. for (let i = 0; i < particles.length; i++) {
  265. if (particles[i].move()) {
  266. alive.push(particles[i]);
  267. }
  268. }
  269. particles = alive;
  270. canvasContext.clearRect(0, 0, canvas.width, canvas.height);
  271. }
  272. function paint() {
  273. canvasContext.globalCompositeOperation = 'source-over';
  274. canvasContext.fillStyle = "transparent";
  275. canvasContext.fillRect(0, 0, w, h);
  276. canvasContext.globalCompositeOperation = 'lighter';
  277. for (let i = 0; i < particles.length; i++) {
  278. particles[i].draw(canvasContext);
  279. }
  280. }
  281. function createFirework() {
  282. xPoint = Math.random() * (w - 200) + 100;
  283. yPoint = Math.random() * (h - 200) + 100;
  284. let nFire = Math.floor(Math.random() * 100) + 100;
  285. let c1 = 'rgb(255,' + Math.floor((Math.random() * 255)) + ',' + Math.floor((Math.random() * 255)) + ')';
  286. let c2 = 'rgb(' + Math.floor((Math.random() * 255)) + ',255,' + Math.floor((Math.random() * 255)) + ')';
  287. let c3 = 'rgb(' + Math.floor((Math.random() * 255)) + ',' + Math.floor((Math.random() * 255)) + ',255)';
  288. let c=[c1,c2,c3]
  289. for (let i = 0; i < nFire; i++) {
  290. let particle = new Particle();
  291. particle.color = c[Math.floor(Math.random() * 3)];
  292. let vy = Math.sqrt(25 - particle.vx * particle.vx);
  293. if (Math.abs(particle.vy) > vy) {
  294. particle.vy = particle.vy > 0 ? vy : -vy;
  295. }
  296. particles.push(particle);
  297. }
  298. }
  299. function Particle() {
  300. this.w = this.h = Math.random() * 6 + 1;
  301. this.x = xPoint - this.w / 2;
  302. this.y = yPoint - this.h / 2;
  303. this.vx = (Math.random() - 0.5) * 10;
  304. this.vy = (Math.random() - 0.5) * 10;
  305. this.alpha = Math.random() * .5 + .5;
  306. this.color = '';
  307. }
  308. Particle.prototype = {
  309. gravity: 0.05,
  310. move: function () {
  311. this.x += this.vx;
  312. this.vy += this.gravity;
  313. this.y += this.vy;
  314. this.alpha -= 0.01;
  315. return !(this.x <= -this.w || this.x >= screen.width ||
  316. this.y >= screen.height ||
  317. this.alpha <= 0);
  318. },
  319. draw: function (c) {
  320. c.save();
  321. c.beginPath();
  322. c.translate(this.x + this.w / 2, this.y + this.h / 2);
  323. c.arc(0, 0, this.w, 0, Math.PI * 2);
  324. c.fillStyle = this.color;
  325. c.globalAlpha = this.alpha;
  326. c.closePath();
  327. c.fill();
  328. c.restore();
  329. }
  330. }
  331. </script>
  332. <style scoped>
  333. :deep(.el-upload-dragger) {
  334. background-color: transparent;
  335. }
  336. .reset-pool {
  337. color: red;
  338. font-size: 12px
  339. }
  340. .reset-pool:hover {
  341. cursor: pointer;
  342. text-decoration: underline;
  343. }
  344. .show-history {
  345. color: #c2c2c2;
  346. }
  347. .show-history:hover {
  348. cursor: pointer;
  349. text-decoration: underline;
  350. }
  351. :deep(.el-checkbox) {
  352. color: white;
  353. }
  354. </style>