You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

885 lines
23 KiB

1 month ago
1 month ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
4 weeks ago
4 weeks ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
4 weeks ago
1 month ago
4 weeks ago
4 weeks ago
1 month ago
1 month ago
1 month ago
1 month ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
1 month ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
1 month ago
  1. <template>
  2. <div class="lottery-3d-container">
  3. <div ref="threeContainer" class="three-container"></div>
  4. <!-- 分页指示器 -->
  5. <div v-if="totalPages > 1" class="page-indicator">
  6. {{ currentPage + 1 }} / {{ totalPages }}
  7. </div>
  8. <!-- 滚动提示 -->
  9. <!-- <div v-if="totalPages > 1 && currentPage === 0" class="scroll-hint">
  10. 向下滚动查看更多
  11. </div>
  12. <div v-if="totalPages > 1 && currentPage === totalPages - 1" class="scroll-hint">
  13. 向上滚动查看上一页
  14. </div> -->
  15. </div>
  16. </template>
  17. <script setup>
  18. import {
  19. ref,
  20. onMounted,
  21. onBeforeUnmount,
  22. defineExpose,
  23. watch,
  24. computed,
  25. } from "vue";
  26. import * as THREE from "three";
  27. import {
  28. CSS3DRenderer,
  29. CSS3DObject,
  30. } from "three/examples/jsm/renderers/CSS3DRenderer.js";
  31. // import { TrackballControls } from 'three/examples/jsm/controls/TrackballControls.js'; // 移除拖拽控件
  32. import TWEEN from "@tweenjs/tween.js";
  33. import { NUMBER_MATRIX } from "../../../utils/config.js";
  34. import CardItem from "./CardItem.vue";
  35. import { createApp } from "vue";
  36. import { getUserListApi } from "../../../api/API";
  37. import { useLotteryStore } from "../../../store/lottery";
  38. const lotteryStore = useLotteryStore();
  39. const winners = computed({
  40. get: () => lotteryStore.winners,
  41. set: (val) => lotteryStore.setWinners(val),
  42. });
  43. const threeContainer = ref(null);
  44. let renderer, scene, camera, animationId;
  45. // let controls; // 移除controls
  46. // 全局变量存储当前选中的卡片索引数组
  47. let globalCardIndexes = [];
  48. // 分页相关变量
  49. const currentPage = ref(0);
  50. const totalPages = ref(0);
  51. const cardsPerPage = 10; // 每页显示的卡片数量
  52. let isPageTransitioning = false; // 防止页面切换时的重复操作
  53. // 3D卡片与目标
  54. const threeDCards = [];
  55. const targets = {
  56. table: [],
  57. sphere: [],
  58. };
  59. function swapCardContents() {
  60. // 确保有足够卡片且不在抽奖状态
  61. if (threeDCards.length < 2 || globalCardIndexes.length > 0) return;
  62. // 随机选择两张不同的卡片
  63. let indexA = Math.floor(Math.random() * threeDCards.length);
  64. let indexB = Math.floor(Math.random() * threeDCards.length);
  65. while (indexA === indexB) {
  66. indexB = Math.floor(Math.random() * threeDCards.length);
  67. }
  68. const cardA = threeDCards[indexA].element;
  69. const cardB = threeDCards[indexB].element;
  70. // 保存原始内容(如果尚未保存)
  71. if (!cardA.dataset.originalContent) {
  72. cardA.dataset.originalContent = cardA.innerHTML;
  73. }
  74. if (!cardB.dataset.originalContent) {
  75. cardB.dataset.originalContent = cardB.innerHTML;
  76. }
  77. // 交换内容并添加动画效果
  78. [cardA.innerHTML, cardB.innerHTML] = [cardB.innerHTML, cardA.innerHTML];
  79. cardA.classList.add('swap-animation');
  80. cardB.classList.add('swap-animation');
  81. // 动画结束后移除动画类
  82. setTimeout(() => {
  83. cardA.classList.remove('swap-animation');
  84. cardB.classList.remove('swap-animation');
  85. }, 500);
  86. }
  87. function createElement(css = "", text = "") {
  88. const dom = document.createElement("div");
  89. dom.className = css;
  90. dom.innerHTML = text;
  91. return dom;
  92. }
  93. function createCard(user, isBold, id, showTable, company) {
  94. // 使用 CardItem 组件动态渲染为 DOM 节点
  95. const container = document.createElement("div");
  96. const app = createApp(CardItem, {
  97. id,
  98. user,
  99. isBold,
  100. showTable,
  101. company,
  102. // highlight, prize 可后续补充
  103. });
  104. app.mount(container);
  105. return container.firstElementChild;
  106. }
  107. function createCards(member, length, showTable, position, config) {
  108. let index = 0;
  109. for (let i = 0; i < config.ROW_COUNT; i++) {
  110. for (let j = 0; j < config.COLUMN_COUNT; j++) {
  111. // 4. 判断是否高亮
  112. const isBold = (config.HIGHLIGHT_CELL || []).includes(j + "-" + i);
  113. const element = createCard(
  114. member[index % length],
  115. isBold,
  116. index,
  117. showTable,
  118. config.COMPANY
  119. );
  120. const object = new CSS3DObject(element);
  121. object.position.x = Math.random() * 4000 - 2000;
  122. object.position.y = Math.random() * 4000 - 2000;
  123. object.position.z = Math.random() * 4000 - 2000;
  124. scene.add(object);
  125. threeDCards.push(object);
  126. const targetObject = new THREE.Object3D();
  127. targetObject.position.x = j * 140 - position.x;
  128. targetObject.position.y = -(i * 180) + position.y;
  129. targets.table.push(targetObject);
  130. index++;
  131. }
  132. }
  133. }
  134. function createSphereTargets() {
  135. const vector = new THREE.Vector3();
  136. for (let i = 0, l = threeDCards.length; i < l; i++) {
  137. const phi = Math.acos(-1 + (2 * i) / l);
  138. const theta = Math.sqrt(l * Math.PI) * phi;
  139. const object = new THREE.Object3D();
  140. object.position.setFromSphericalCoords(600, phi, theta);
  141. object.position.y -= 200; // 向下偏移200px
  142. // 修正朝向计算:让卡牌朝向球体中心点
  143. vector.set(0, -200, 0); // 球体中心点,Y轴偏移与上面保持一致
  144. object.lookAt(vector);
  145. targets.sphere.push(object);
  146. }
  147. }
  148. // 动画与切换相关方法
  149. function switchScreen(type) {
  150. if (highlightTimeout) {
  151. clearTimeout(highlightTimeout);
  152. highlightTimeout = null;
  153. }
  154. // 示例:enter/table/sphere 切换
  155. if (type === "enter") {
  156. transform(targets.table, 2000, () => {
  157. addHighlight();
  158. highlightTimeout = null;
  159. }); // 动画结束后加高亮
  160. } else {
  161. transform(targets.sphere, 2000, () => removeHighlight()); // 动画结束后移除高亮
  162. }
  163. }
  164. function transform(targetsArr, duration, onComplete) {
  165. for (let i = 0; i < threeDCards.length; i++) {
  166. const object = threeDCards[i];
  167. const target = targetsArr[i];
  168. new TWEEN.Tween(object.position)
  169. .to(
  170. {
  171. x: target.position.x,
  172. y: target.position.y,
  173. z: target.position.z,
  174. },
  175. Math.random() * duration + duration
  176. )
  177. .easing(TWEEN.Easing.Exponential.InOut)
  178. .start();
  179. new TWEEN.Tween(object.rotation)
  180. .to(
  181. {
  182. x: target.rotation.x,
  183. y: target.rotation.y,
  184. z: target.rotation.z,
  185. },
  186. Math.random() * duration + duration
  187. )
  188. .easing(TWEEN.Easing.Exponential.InOut)
  189. .start();
  190. }
  191. new TWEEN.Tween({})
  192. .to({}, duration * 2)
  193. .onUpdate(() => render())
  194. .onComplete(() => {
  195. if (onComplete) onComplete();
  196. })
  197. .start();
  198. }
  199. function selectCard(selectedCardIndex, currentLuckys, duration = 600) {
  200. if (highlightTimeout) {
  201. clearTimeout(highlightTimeout);
  202. highlightTimeout = null;
  203. }
  204. removeHighlight(); // 开始抽奖前移除高亮
  205. console.log("selectCard called:", {
  206. selectedCardIndex,
  207. currentLuckys,
  208. duration,
  209. });
  210. return new Promise((resolve) => {
  211. const width = 140;
  212. // 计算总页数
  213. totalPages.value = Math.ceil(currentLuckys.length / cardsPerPage);
  214. currentPage.value = 0;
  215. // 为每页计算位置信息
  216. const pageLocates = [];
  217. for (let page = 0; page < totalPages.value; page++) {
  218. const startIndex = page * cardsPerPage;
  219. const endIndex = Math.min(
  220. (page + 1) * cardsPerPage,
  221. currentLuckys.length
  222. );
  223. const pageCount = endIndex - startIndex;
  224. const pageLocate = [];
  225. // 根据当前页的人数决定排列方式
  226. if (pageCount > 5) {
  227. // 大于5个分两排显示(与抽10人时的排列相同)
  228. const yPosition = [-87, 87];
  229. const mid = Math.ceil(pageCount / 2);
  230. let tag = -(mid - 1) / 2;
  231. for (let i = 0; i < mid; i++) {
  232. pageLocate.push({
  233. x: tag * width,
  234. y: yPosition[0],
  235. });
  236. tag++;
  237. }
  238. tag = -(pageCount - mid - 1) / 2;
  239. for (let i = mid; i < pageCount; i++) {
  240. pageLocate.push({
  241. x: tag * width,
  242. y: yPosition[1],
  243. });
  244. tag++;
  245. }
  246. } else {
  247. // 小于等于5个一排显示(与抽不足10人时的排列相同)
  248. let tag = -(pageCount - 1) / 2;
  249. for (let i = 0; i < pageCount; i++) {
  250. pageLocate.push({
  251. x: tag * width,
  252. y: 0,
  253. });
  254. tag++;
  255. }
  256. }
  257. pageLocates.push(pageLocate);
  258. }
  259. console.log("pageLocates calculated:", pageLocates);
  260. // 初始化所有卡片位置(隐藏超出第一页的卡片)
  261. selectedCardIndex.forEach((cardIndex, index) => {
  262. changeCard(cardIndex, currentLuckys[index]);
  263. const object = threeDCards[cardIndex];
  264. // 计算卡片应该在第几页
  265. const cardPage = Math.floor(index / cardsPerPage);
  266. const isVisible = cardPage === 0;
  267. // 计算在当前页中的索引
  268. const pageIndex = index % cardsPerPage;
  269. const pageLocate = pageLocates[cardPage][pageIndex];
  270. // 设置初始位置:第一页的卡片正常显示,其他页的卡片从下方隐藏
  271. let initialY;
  272. if (isVisible) {
  273. initialY = pageLocate.y;
  274. } else {
  275. // 非第一页的卡片从下方隐藏(为后续向上飞出做准备)
  276. initialY = pageLocate.y - 1000;
  277. }
  278. new TWEEN.Tween(object.position)
  279. .to(
  280. {
  281. x: pageLocate.x,
  282. y: initialY,
  283. z: 2200,
  284. },
  285. Math.random() * duration + duration
  286. )
  287. .easing(TWEEN.Easing.Exponential.InOut)
  288. .start();
  289. new TWEEN.Tween(object.rotation)
  290. .to(
  291. {
  292. x: 0,
  293. y: 0,
  294. z: 0,
  295. },
  296. Math.random() * duration + duration
  297. )
  298. .easing(TWEEN.Easing.Exponential.InOut)
  299. .start();
  300. object.element.classList.add("prize");
  301. });
  302. // 保存页面位置信息到全局变量,供switchPage使用
  303. window.pageLocates = pageLocates;
  304. new TWEEN.Tween({})
  305. .to({}, duration * 2)
  306. .onUpdate(() => render())
  307. .start()
  308. .onComplete(() => {
  309. console.log("selectCard animation completed");
  310. // 如果有多页,添加鼠标滚轮事件监听
  311. if (totalPages.value > 1) {
  312. addWheelListener();
  313. }
  314. resolve();
  315. });
  316. });
  317. }
  318. // 分页切换函数
  319. function switchPage(direction) {
  320. if (isPageTransitioning || totalPages.value <= 1) return;
  321. const newPage =
  322. direction === "next" ? currentPage.value + 1 : currentPage.value - 1;
  323. if (newPage < 0 || newPage >= totalPages.value) return;
  324. isPageTransitioning = true;
  325. const duration = 800;
  326. // 使用保存的页面位置信息
  327. const pageLocates = window.pageLocates;
  328. if (!pageLocates) {
  329. console.error("页面位置信息未找到");
  330. isPageTransitioning = false;
  331. return;
  332. }
  333. console.log("globalCardIndexes", globalCardIndexes);
  334. // 动画切换卡片位置
  335. globalCardIndexes.forEach((cardIndex, index) => {
  336. const object = threeDCards[cardIndex];
  337. const cardPage = Math.floor(index / cardsPerPage);
  338. const isVisible = cardPage === newPage;
  339. const wasVisible = cardPage === currentPage.value;
  340. // 计算在当前页中的索引
  341. const pageIndex = index % cardsPerPage;
  342. const pageLocate = pageLocates[cardPage][pageIndex];
  343. // 根据切换方向决定动画效果
  344. let targetY;
  345. if (isVisible) {
  346. // 当前页要显示的卡片
  347. if (direction === "next") {
  348. // 索引增大:从下方飞出
  349. targetY = pageLocate.y;
  350. } else {
  351. // 索引减少:从上方飞出
  352. targetY = pageLocate.y;
  353. }
  354. } else {
  355. // 当前页要隐藏的卡片
  356. if (direction === "next") {
  357. // 索引增大:向上飞走
  358. targetY = pageLocate.y + 1000;
  359. } else {
  360. // 索引减少:向下飞走
  361. targetY = pageLocate.y - 1000;
  362. }
  363. }
  364. // 设置起始位置
  365. let startY;
  366. if (wasVisible) {
  367. // 当前页的卡片从当前位置开始
  368. startY = object.position.y;
  369. } else {
  370. // 非当前页的卡片从隐藏位置开始
  371. if (direction === "next") {
  372. // 索引增大:从下方开始
  373. startY = pageLocate.y - 1000;
  374. } else {
  375. // 索引减少:从上方开始
  376. startY = pageLocate.y + 1000;
  377. }
  378. }
  379. // 先设置起始位置
  380. object.position.y = startY;
  381. new TWEEN.Tween(object.position)
  382. .to(
  383. {
  384. x: pageLocate.x,
  385. y: targetY,
  386. z: 2200,
  387. },
  388. duration
  389. )
  390. .easing(TWEEN.Easing.Cubic.InOut)
  391. .start();
  392. });
  393. new TWEEN.Tween({})
  394. .to({}, duration)
  395. .onUpdate(() => render())
  396. .onComplete(() => {
  397. currentPage.value = newPage;
  398. isPageTransitioning = false;
  399. console.log(
  400. `切换到第 ${currentPage.value + 1} 页,共 ${totalPages.value}`
  401. );
  402. })
  403. .start();
  404. }
  405. // 鼠标滚轮事件处理
  406. function handleWheel(event) {
  407. if (isPageTransitioning || totalPages.value <= 1) return;
  408. event.preventDefault();
  409. if (event.deltaY > 0) {
  410. // 向下滚动,显示下一页
  411. switchPage("next");
  412. } else if (event.deltaY < 0) {
  413. // 向上滚动,显示上一页
  414. switchPage("prev");
  415. }
  416. }
  417. // 添加鼠标滚轮事件监听器
  418. function addWheelListener() {
  419. if (threeContainer.value) {
  420. threeContainer.value.addEventListener("wheel", handleWheel, {
  421. passive: false,
  422. });
  423. }
  424. }
  425. // 移除鼠标滚轮事件监听器
  426. function removeWheelListener() {
  427. if (threeContainer.value) {
  428. threeContainer.value.removeEventListener("wheel", handleWheel);
  429. }
  430. }
  431. function resetCard(selectedCardIndex, duration = 500) {
  432. if (!selectedCardIndex || selectedCardIndex.length === 0) {
  433. return Promise.resolve();
  434. }
  435. // 移除鼠标滚轮事件监听器
  436. removeWheelListener();
  437. // 重置分页状态
  438. currentPage.value = 0;
  439. totalPages.value = 0;
  440. isPageTransitioning = false;
  441. // 清理保存的页面位置信息
  442. if (window.pageLocates) {
  443. delete window.pageLocates;
  444. }
  445. // 清空全局卡片索引数组
  446. globalCardIndexes = [];
  447. selectedCardIndex.forEach((index) => {
  448. const object = threeDCards[index];
  449. const target = targets.sphere[index];
  450. new TWEEN.Tween(object.position)
  451. .to(
  452. {
  453. x: target.position.x,
  454. y: target.position.y,
  455. z: target.position.z,
  456. },
  457. Math.random() * duration + duration
  458. )
  459. .easing(TWEEN.Easing.Exponential.InOut)
  460. .start();
  461. new TWEEN.Tween(object.rotation)
  462. .to(
  463. {
  464. x: target.rotation.x,
  465. y: target.rotation.y,
  466. z: target.rotation.z,
  467. },
  468. Math.random() * duration + duration
  469. )
  470. .easing(TWEEN.Easing.Exponential.InOut)
  471. .start();
  472. });
  473. return new Promise((resolve) => {
  474. new TWEEN.Tween({})
  475. .to({}, duration * 2)
  476. .onUpdate(() => render())
  477. .start()
  478. .onComplete(() => {
  479. selectedCardIndex.forEach((index) => {
  480. const object = threeDCards[index];
  481. // 恢复原始内容
  482. if (object.element.dataset.originalContent) {
  483. object.element.innerHTML = object.element.dataset.originalContent;
  484. delete object.element.dataset.originalContent;
  485. }
  486. object.element.classList.remove("prize");
  487. });
  488. resolve();
  489. });
  490. });
  491. }
  492. function changeCard(cardIndex, user) {
  493. // 保存到全局变量数组
  494. if (!globalCardIndexes.includes(cardIndex)) {
  495. globalCardIndexes.push(cardIndex);
  496. }
  497. const card = threeDCards[cardIndex].element;
  498. // 保存原始内容,以便后续恢复
  499. if (!card.dataset.originalContent) {
  500. card.dataset.originalContent = card.innerHTML;
  501. }
  502. // 设置中奖内容 - 适配后端返回的数据格式
  503. // 后端返回的数据格式: { jwcode: "5412", username: "猪八戒22" }
  504. const jwcode = user.jwcode || user[0] || "";
  505. const username = user.username || user[1] || "";
  506. const company = user.company || user[2] || "PSST";
  507. card.innerHTML = `<div style="font-size: 20px; font-weight: bold; color: #ffffff; text-align: center; display: flex; justify-content: center; align-items: center; width: 100%; height: 100%;">${jwcode}</div>`;
  508. // 添加中奖样式类
  509. card.classList.add("prize");
  510. }
  511. function changeCard1() {
  512. console.log("执行取消高光,当前卡片索引:", globalCardIndexes);
  513. // 移除鼠标滚轮事件监听器
  514. removeWheelListener();
  515. // 重置分页状态
  516. currentPage.value = 0;
  517. totalPages.value = 0;
  518. isPageTransitioning = false;
  519. // 清理保存的页面位置信息
  520. if (window.pageLocates) {
  521. delete window.pageLocates;
  522. }
  523. globalCardIndexes.forEach((cardIndex) => {
  524. const card = threeDCards[cardIndex].element;
  525. // console.log('取消卡片', cardIndex, '的高光');
  526. // 恢复原始内容
  527. if (card.dataset.originalContent) {
  528. card.innerHTML = card.dataset.originalContent;
  529. delete card.dataset.originalContent;
  530. }
  531. // 移除prize类,让CardItem组件的样式重新生效
  532. card.classList.remove("prize");
  533. });
  534. // 清空数组
  535. globalCardIndexes = [];
  536. }
  537. function shine(cardIndex, color) {
  538. const card = threeDCards[cardIndex].element;
  539. card.style.backgroundColor =
  540. color || `rgba(0,127,127,${Math.random() * 0.7 + 0.25})`;
  541. }
  542. // 响应式高亮索引
  543. const highlightedIndexes = ref([]);
  544. // 替换 addHighlight 和 removeHighlight 为响应式写法
  545. function addHighlight(indexes = null) {
  546. if (indexes) {
  547. highlightedIndexes.value = [...indexes];
  548. } else {
  549. // 默认高亮所有 .lightitem
  550. highlightedIndexes.value = threeDCards
  551. // .map((obj, idx) =>
  552. // obj.element.classList.contains("lightitem") ? idx : null
  553. // )
  554. .filter((idx) => idx !== null);
  555. }
  556. }
  557. function removeHighlight(indexes = null) {
  558. if (indexes) {
  559. highlightedIndexes.value = highlightedIndexes.value.filter(
  560. (i) => !indexes.includes(i)
  561. );
  562. } else {
  563. highlightedIndexes.value = [];
  564. }
  565. }
  566. // 监听高亮索引变化并同步到DOM
  567. watch(highlightedIndexes, (newVal) => {
  568. threeDCards.forEach((cardObj, idx) => {
  569. if (newVal.includes(idx)) {
  570. cardObj.element.classList.add("highlight");
  571. } else {
  572. cardObj.element.classList.remove("highlight");
  573. }
  574. });
  575. });
  576. let rotateObj = null;
  577. let highlightTimeout = null;
  578. function rotateBallStart() {
  579. return new Promise((resolve) => {
  580. if (!scene) return resolve();
  581. scene.rotation.y = 0;
  582. rotateObj = new TWEEN.Tween(scene.rotation)
  583. .to({ y: Math.PI * 6 * 1000 }, 3000 * 1000)
  584. .onUpdate(() => render())
  585. .onComplete(() => resolve())
  586. .start();
  587. });
  588. }
  589. function rotateBallStop() {
  590. return new Promise((resolve) => {
  591. if (!scene || !rotateObj) return resolve();
  592. rotateObj.stop();
  593. // 完全还原原生补偿动画逻辑
  594. const currentY = scene.rotation.y;
  595. const targetY = Math.ceil(currentY / (2 * Math.PI)) * 2 * Math.PI;
  596. const deltaY = Math.abs(targetY - currentY);
  597. const duration = 500 + 1000 * (deltaY / Math.PI);
  598. new TWEEN.Tween(scene.rotation)
  599. .to({ y: targetY }, duration)
  600. .easing(TWEEN.Easing.Cubic.Out)
  601. .onUpdate(() => render())
  602. .onComplete(() => {
  603. scene.rotation.y = 0;
  604. render();
  605. resolve();
  606. })
  607. .start();
  608. });
  609. }
  610. function getTotalCards() {
  611. return threeDCards.length;
  612. }
  613. onMounted(async () => {
  614. // 初始化 3D 场景
  615. scene = new THREE.Scene();
  616. camera = new THREE.PerspectiveCamera(
  617. 40,
  618. window.innerWidth / window.innerHeight,
  619. 1,
  620. 10000
  621. );
  622. camera.position.z = 3000;
  623. // camera.position.y = 250; // 整体上移10px
  624. // 或
  625. // scene.position.y = 10; // 整体上移10px
  626. renderer = new CSS3DRenderer();
  627. renderer.setSize(window.innerWidth, window.innerHeight);
  628. if (threeContainer.value) {
  629. threeContainer.value.appendChild(renderer.domElement);
  630. }
  631. // 2. 生成高亮坐标(以数字8为例)
  632. let text = "0123";
  633. let step = 5;
  634. let xoffset = 1;
  635. let yoffset = 1;
  636. let highlight = [];
  637. text.split("").forEach((n) => {
  638. highlight = highlight.concat(
  639. NUMBER_MATRIX[n].map((item) => {
  640. return `${item[0] + xoffset}-${item[1] + yoffset}`;
  641. })
  642. );
  643. xoffset += step;
  644. });
  645. // const highlightCells = NUMBER_MATRIX["0"].map(([x, y]) => `${x}-${y}`);
  646. const config = {
  647. ROW_COUNT: 7, // 数字矩阵是5行
  648. COLUMN_COUNT: 21, // 数字矩阵是4列
  649. HIGHLIGHT_CELL: highlight,
  650. COMPANY: "演示公司",
  651. };
  652. const userList = await getUserListApi();
  653. console.log("3D调用一次接口", userList);
  654. // lotteryStore.setWinners(userList);
  655. // console.log("userList", userList);
  656. // 将用户数据转换为兼容格式,用于3D卡片显示
  657. const member = userList.data.map((item) => [
  658. item.jwcode,
  659. item.username,
  660. "PSST",
  661. ]);
  662. // 将用户列表存储到store中,用于卡牌文字切换
  663. const userNames = userList.data.map(
  664. (item) => item.jwcode || item.username || ""
  665. );
  666. lotteryStore.setAllUsers(userNames);
  667. const length = member.length;
  668. const showTable = true;
  669. const position = {
  670. x: (100 * config.COLUMN_COUNT - 20) / 2,
  671. y: (120 * config.ROW_COUNT - 20) / 2,
  672. };
  673. createCards(member, length, showTable, position, config); // 3. 传递高亮配置
  674. createSphereTargets();
  675. // 先渲染散落状态
  676. render();
  677. animate();
  678. // 延迟后自动聚集到界面中间
  679. setTimeout(() => {
  680. switchScreen("enter");
  681. }, 500);
  682. window.addEventListener("resize", onWindowResize);
  683. // swapInterval.value = setInterval(swapCardContents, swapIntervalTime.value);
  684. });
  685. onBeforeUnmount(() => {
  686. window.removeEventListener("resize", onWindowResize);
  687. removeWheelListener(); // 移除鼠标滚轮事件监听器
  688. if (animationId) cancelAnimationFrame(animationId);
  689. if (highlightTimeout) {
  690. clearTimeout(highlightTimeout);
  691. highlightTimeout = null;
  692. }
  693. // if (swapInterval.value) {
  694. // clearInterval(swapInterval.value);
  695. // swapInterval.value = null;
  696. // }
  697. });
  698. function render() {
  699. renderer.render(scene, camera);
  700. }
  701. function animate() {
  702. animationId = requestAnimationFrame(animate);
  703. TWEEN.update();
  704. // controls.update(); // 移除controls
  705. render();
  706. }
  707. function onWindowResize() {
  708. camera.aspect = window.innerWidth / window.innerHeight;
  709. camera.updateProjectionMatrix();
  710. renderer.setSize(window.innerWidth, window.innerHeight);
  711. render();
  712. }
  713. defineExpose({
  714. resetCard,
  715. addHighlight,
  716. switchScreen,
  717. rotateBallStart,
  718. rotateBallStop,
  719. selectCard,
  720. getTotalCards,
  721. changeCard1,
  722. });
  723. </script>
  724. <style scoped>
  725. .lottery-3d-container {
  726. width: 100vw;
  727. height: 100vh;
  728. overflow: hidden;
  729. position: relative;
  730. }
  731. .three-container {
  732. width: 100%;
  733. height: 100%;
  734. }
  735. /* 分页指示器样式 */
  736. .page-indicator {
  737. position: absolute;
  738. bottom: 20px;
  739. left: 50%;
  740. transform: translateX(-50%);
  741. z-index: 1000;
  742. background: rgba(0, 0, 0, 0.7);
  743. color: white;
  744. padding: 8px 16px;
  745. border-radius: 20px;
  746. font-size: 14px;
  747. pointer-events: none;
  748. }
  749. /* 滚动提示样式 */
  750. .scroll-hint {
  751. position: absolute;
  752. bottom: 60px;
  753. left: 50%;
  754. transform: translateX(-50%);
  755. z-index: 1000;
  756. background: rgba(0, 0, 0, 0.7);
  757. color: white;
  758. padding: 8px 16px;
  759. border-radius: 20px;
  760. font-size: 12px;
  761. pointer-events: none;
  762. animation: fadeInOut 2s ease-in-out infinite;
  763. }
  764. .price-font {
  765. font-size: 16px;
  766. font-weight: bold;
  767. color: #ffffff;
  768. text-align: center;
  769. display: flex;
  770. justify-content: center;
  771. align-items: center;
  772. width: 100%;
  773. height: 100%;
  774. }
  775. @keyframes fadeInOut {
  776. 0%,
  777. 100% {
  778. opacity: 0.6;
  779. }
  780. 50% {
  781. opacity: 1;
  782. }
  783. }
  784. </style>