RedPacketModal.tsx 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584
  1. "use client";
  2. import { receiveRedPacketApi } from "@/api/promo";
  3. import { useRouter } from "@/i18n/routing";
  4. import { getToken } from "@/utils/Cookies";
  5. import { Mask } from "antd-mobile";
  6. import clsx from "clsx";
  7. import { useTranslations } from "next-intl";
  8. import Image from "next/image";
  9. import {
  10. forwardRef,
  11. Fragment,
  12. memo,
  13. useEffect,
  14. useImperativeHandle,
  15. useRef,
  16. useState,
  17. } from "react";
  18. import styles from "./redpacked.module.scss";
  19. const randomX = (len: number) => {
  20. return Math.floor(Math.random() * len);
  21. };
  22. const mockData = Array(500)
  23. .fill(0)
  24. .map((item) => {
  25. return {
  26. phone: `55****${(randomX(99) + "").padEnd(2, "0")}`,
  27. num: `${(Math.random() * 20).toFixed(2)}`,
  28. time: "11:00",
  29. };
  30. });
  31. function getRandom(min: number, max: number) {
  32. const floatRandom = Math.random();
  33. const difference = max - min;
  34. // 介于 0 和差值之间的随机数
  35. const random = Math.round(difference * floatRandom);
  36. return random + min;
  37. }
  38. /**
  39. * @description 描述
  40. */
  41. type DescProps = {
  42. onClose: () => void;
  43. };
  44. const Desc = (props: DescProps) => {
  45. const { onClose } = props;
  46. const [activeTab, setActiveTab] = useState(0);
  47. const tabs = [{ text: "Vezes de evento participado" }, { text: "Consulta de registo levado" }];
  48. return (
  49. <div className={"absolute top-[30%] w-[100%]"}>
  50. <div className={"absolute -top-[1.3rem] left-1/2 w-[3.3rem] -translate-x-1/2"}>
  51. <img src="/9f/red-header.png" alt="" />
  52. <div
  53. className={
  54. "h-[0.2rem] w-[0.2rem] " + " absolute bottom-[20px] right-[30px]" + " "
  55. }
  56. onClick={onClose}
  57. >
  58. <Image src={"/9f/close.png"} alt={"close"} width={25} height={25} />
  59. </div>
  60. </div>
  61. <div
  62. className={"mx-auto w-[2.9rem] bg-[#ff9417] px-[0.16rem] pb-[0.12rem] pt-[0.44rem]"}
  63. >
  64. <img src="/9f/red-title.png" alt="" className={"mx-auto w-[90%]"} />
  65. <div className={"mt-[0.0694rem] rounded-[3px] bg-primary-color"}>
  66. <div className={"flex h-[0.54rem] justify-between text-center text-[0.15rem]"}>
  67. {tabs.map((item, index) => {
  68. return (
  69. <Fragment key={index}>
  70. <span
  71. onClick={() => setActiveTab(index)}
  72. className={`flex h-[100%] items-center ${index === activeTab ? "bg-[#8b3500] text-[#5f2600]" : ""}`}
  73. >
  74. {item.text}
  75. </span>
  76. </Fragment>
  77. );
  78. })}
  79. </div>
  80. {/* 动态内容 */}
  81. <div className={"h-[3rem] overflow-y-scroll p-[0.1rem]"}>
  82. <div
  83. className={
  84. "flex items-center rounded-[0.1rem] bg-[#d45300] p-[10px] font-bold"
  85. }
  86. >
  87. <Image
  88. className={"h-[0.43rem] w-[0.7rem]"}
  89. width={80}
  90. height={40}
  91. src="/9f/wallet.png"
  92. alt=""
  93. />
  94. <div className={"text-center"}>
  95. <h2> Tempo regressivo </h2>
  96. <p className={"text-[#fe0]"}>Começa amanhã às 11:00</p>
  97. </div>
  98. </div>
  99. {!activeTab ? (
  100. <>
  101. <Image
  102. src={"/9f/6xtime.png"}
  103. className={"mt-[0.1rem] h-[0.7rem] w-[100%] rounded-[0.1rem]"}
  104. width={600}
  105. height={100}
  106. alt={"time"}
  107. ></Image>
  108. <Image
  109. src={"/9f/3xtime.png"}
  110. className={"mt-[0.1rem] h-[0.55rem] w-[100%] rounded-[0.1rem]"}
  111. width={600}
  112. height={80}
  113. alt={"time"}
  114. ></Image>
  115. </>
  116. ) : (
  117. <>
  118. <div
  119. className={"mt-[0.1rem] rounded-[0.1rem] bg-[#d45300] p-[10px]"}
  120. >
  121. <p className={"text-[#fe0]"}>Registro persoal</p>
  122. <div
  123. className={
  124. "grid grid-cols-3 text-center" +
  125. " items-center text-[0.1rem]"
  126. }
  127. >
  128. <span>Nome do jogo</span>
  129. <span>Coleta cumulativa</span>
  130. <span>Participação total</span>
  131. </div>
  132. <div
  133. className={
  134. "grid grid-cols-3 text-center" +
  135. " items-center text-[0.12rem] font-bold"
  136. }
  137. >
  138. <span>55****26</span>
  139. <span>R$ 100.00</span>
  140. <span>1</span>
  141. </div>
  142. </div>
  143. <div
  144. className={"mt-[0.1rem] rounded-[0.1rem] bg-[#d45300] p-[10px]"}
  145. >
  146. <p className={"text-[#fe0]"}>Lista dos vencedores</p>
  147. <div
  148. className={
  149. "grid grid-cols-3 text-center" +
  150. " items-center text-[0.1rem]"
  151. }
  152. >
  153. <span>ID do papel</span>
  154. <span>Valor obtido</span>
  155. <span>Hora obtida</span>
  156. </div>
  157. <div className={`h-[1rem] overflow-hidden`}>
  158. {mockData.map((item, index) => {
  159. return (
  160. <div
  161. key={index}
  162. className={`grid grid-cols-3 items-center text-center text-[0.12rem] font-bold ${styles.scrollAnimation}`}
  163. >
  164. <span>{item.phone}</span>
  165. <span>R$ {item.num}</span>
  166. <span>{item.time}</span>
  167. </div>
  168. );
  169. })}
  170. </div>
  171. </div>
  172. </>
  173. )}
  174. <ul className={"mt-[0.1rem] text-[0.1rem]"}>
  175. <li>
  176. ·Cada sessão de chuva de dinheiro é distribuída gratuitamente por
  177. R$100.000.
  178. </li>
  179. <li>
  180. ·Valor máximo de queda em dinheiro: Cada sessão de chuva de dinheiro
  181. é distribuída gratuitamente.
  182. </li>
  183. <li>·Membros recarregados podem reivindicar gratuitamente.</li>
  184. <li>
  185. ·O dinheiro recebido pode ser utilizado para jogar ou sacar
  186. diretamente.
  187. </li>
  188. <li>
  189. ·Quanto for maior o nível de associação VIP, será maior o valor
  190. recebido.
  191. </li>
  192. </ul>
  193. </div>
  194. </div>
  195. </div>
  196. </div>
  197. );
  198. };
  199. // 红包掉落动画像下雨
  200. const FallAnimation1 = (props: any) => {
  201. const { onClose } = props;
  202. const fallContentRef = useRef<HTMLDivElement>(null);
  203. const isActive = useRef(true);
  204. const getRandom = (min: number, max: number) => {
  205. return Math.random() * (max - min) + min;
  206. };
  207. const createMoneyElement = (xPos: number) => {
  208. if (!fallContentRef.current || !isActive.current) return;
  209. const money = document.createElement('div');
  210. // 基础样式
  211. money.style.cssText = `
  212. position: fixed;
  213. left: ${xPos}px;
  214. top: -100px;
  215. width: 60px;
  216. height: 60px;
  217. pointer-events: none;
  218. `;
  219. // 创建图片
  220. const img = document.createElement('img');
  221. img.src = `/9f/money${Math.floor(Math.random() * 3) + 1}.png`;
  222. img.style.cssText = `
  223. width: 100%;
  224. height: 100%;
  225. object-fit: contain;
  226. `;
  227. money.appendChild(img);
  228. fallContentRef.current.appendChild(money);
  229. // 使用 requestAnimationFrame 确保元素已添加到 DOM 后再添加动画
  230. requestAnimationFrame(() => {
  231. const scale = getRandom(0.4, 1);
  232. const duration = getRandom(8, 12);
  233. const delay = getRandom(0, 5);
  234. money.style.cssText += `
  235. transform: scale(${scale});
  236. z-index: ${Math.floor(scale * 100)};
  237. animation: ${styles.fall} ${duration}s linear infinite,
  238. ${styles.sway} ${duration/2}s ease-in-out infinite alternate;
  239. animation-delay: ${delay}s;
  240. `;
  241. });
  242. // 当动画完成一次循环后,重新设置位置
  243. setInterval(() => {
  244. if (isActive.current) {
  245. money.style.left = `${getRandom(0, window.innerWidth)}px`;
  246. }
  247. }, 12000);
  248. };
  249. useEffect(() => {
  250. console.log('FallAnimation1 mounted'); // 调试日志
  251. if (!fallContentRef.current) return;
  252. // 设置容器样式
  253. fallContentRef.current.style.cssText = `
  254. position: fixed;
  255. top: 0;
  256. left: 0;
  257. width: 100%;
  258. height: 100vh;
  259. z-index: 1000;
  260. pointer-events: none;
  261. overflow: hidden;
  262. background: transparent;
  263. `;
  264. // 分批创建元素
  265. const totalElements = 300;
  266. const batchSize = 20;
  267. let created = 0;
  268. const createBatch = () => {
  269. if (!isActive.current) return;
  270. for (let i = 0; i < batchSize && created < totalElements; i++) {
  271. const xPos = getRandom(0, window.innerWidth);
  272. createMoneyElement(xPos);
  273. created++;
  274. }
  275. if (created < totalElements) {
  276. setTimeout(createBatch, 200);
  277. }
  278. };
  279. createBatch();
  280. return () => {
  281. console.log('FallAnimation1 unmounting'); // 调试日志
  282. isActive.current = false;
  283. };
  284. }, []);
  285. return (
  286. <div
  287. ref={fallContentRef}
  288. style={{
  289. position: 'fixed',
  290. top: 0,
  291. left: 0,
  292. width: '100%',
  293. height: '100vh',
  294. zIndex: 1000,
  295. }}
  296. onClick={(e) => {
  297. e.stopPropagation();
  298. onClose?.();
  299. }}
  300. />
  301. );
  302. };
  303. const HbyInfoDetail = (props: any) => {
  304. const { iconImg, onCloseHby } = props;
  305. const router = useRouter();
  306. const token = getToken();
  307. const handler = () => {
  308. if (!token) {
  309. router.push("/login");
  310. }
  311. onCloseHby();
  312. };
  313. return (
  314. <div
  315. className={`absolute left-1/2 top-[50%] w-[90%] -translate-x-1/2 -translate-y-1/2 ${styles.promoRules}`}
  316. >
  317. {/* <Image src={"/hby/close.png"} alt={"close"} width={25} height={25} onClick={onCloseHby} className={styles.closeIcon}/> */}
  318. <div onClick={onCloseHby} className={styles.closeIcon}></div>
  319. <Image src={iconImg} onClick={handler} alt={"detail"} width={672} height={1044} />
  320. {/* <div className={`h-[0.15rem] text-[#ffd800] text-[0.20rem] text-center ${styles.promoTitle}`}>Dinheiro como chuva</div>
  321. <div className={styles.titleWrap}>
  322. <span>R$200.00</span>
  323. <span> por vez, </span>
  324. <span>Máx queda </span>
  325. <span>R$7.777</span>
  326. </div>
  327. <div className={styles.tips}>
  328. <img src="/hby/tip-icon.png" alt="tips" className={styles.tipsIcon}/>
  329. <div className={styles.tipsTime}>Começa às 23:00</div>
  330. </div>
  331. <div className={styles.times1}>
  332. <img src="/hby/time1.png"/>
  333. </div>
  334. <div className={styles.times2}>
  335. <img src="/hby/time2.png"/>
  336. </div>
  337. <ul className={styles.rulelist}>
  338. <li className={styles.ruleItem}>
  339. Cada sessão de chuva de dinheiro é distribuída gratuitamente com <span>R$200.000</span>
  340. </li>
  341. <li className={styles.ruleItem}>
  342. Valor máximo de queda em dinheiro:Cada sessäo de chuva de dinheiro é distribuida gratuitamente com
  343. </li>
  344. <li className={styles.ruleItem}>
  345. Membros recarregados podem reivindicar gratuitamente
  346. </li>
  347. <li className={styles.ruleItem}>
  348. O dinheiro recebido pode ser utilizado para jogar ou sacado diretamente
  349. </li>
  350. <li className={styles.ruleItem}>
  351. Quanto maior o nivel de associacäo VP, maior o valor recebido
  352. </li>
  353. </ul> */}
  354. </div>
  355. );
  356. };
  357. const HbyInfo = (props: any) => {
  358. const { iconImg, onCloseHby, onReciveRed } = props;
  359. if (!iconImg) return;
  360. return (
  361. <div
  362. className={`absolute left-1/2 top-[50%] -translate-x-1/2 -translate-y-1/2 ${styles.redclose}`}
  363. >
  364. {/* <Image src={"/hby/close.png"} alt={"close"} width={20} height={20} onClick={onCloseHby} className={styles.closeIcon}/> */}
  365. <div onClick={onCloseHby} className={styles.closeIcon}></div>
  366. <img
  367. src={iconImg}
  368. alt={"icon"}
  369. width={559}
  370. height={687}
  371. onClick={onReciveRed}
  372. className={styles.redIcon}
  373. />
  374. {/* <div className={styles.title}>Chuva de dinheiro</div>
  375. <div className={styles.desc}>
  376. <ul className={styles.desclist}>
  377. <li className={styles.descitem}> Membros recarregados podem reivindicar gratuitamente. </li>
  378. <div className={styles.line}></div>
  379. <li className={styles.descitem}> Valor máximo de queda em dinheiro: R$7.777 </li>
  380. </ul>
  381. </div>
  382. <div className={styles.openBtn} onClick={onReciveRed}>AGARRAR</div> */}
  383. </div>
  384. // <div data-v-f333135e="" className={`absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 ${styles.redopen}`}>
  385. // <Image src={"/hby/close.png"} alt={"close"} width={20} height={20} onClick={onCloseHby} className={styles.closeIcon}/>
  386. // <div className={styles.title}>Chuva de dinheiro</div>
  387. // <div className={styles.cash}>1.96</div>
  388. // <div className={styles.tips}>Valor máximo de queda em dinheiro:Cada sessäo de chuva de dinheiro é </div>
  389. // </div>
  390. );
  391. };
  392. const HbyInfo2 = (props: any) => {
  393. const { iconImg, onCloseHby, redAmount } = props;
  394. const hbyInfoClass = clsx(
  395. `${styles.redopen} absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 bg-no-repeat bg-cover`
  396. );
  397. const t = useTranslations("packetsPopup");
  398. return (
  399. <div
  400. className={hbyInfoClass}
  401. style={{ background: `url(${iconImg})`, backgroundSize: "100% 100%" }}
  402. >
  403. <Image
  404. src={"/hby/close.png"}
  405. alt={"close"}
  406. width={30}
  407. height={30}
  408. onClick={onCloseHby}
  409. className={styles.closeIcon}
  410. />
  411. <div className={styles.title}>{t("title")}</div>
  412. <div className={styles.cash}>{redAmount}</div>
  413. <div className={styles.tips}>
  414. {redAmount > 0 ? t("receiveSuccess") : t("receiveWarring")}
  415. </div>
  416. </div>
  417. );
  418. };
  419. type Props = {
  420. onAfterHandler?: () => void;
  421. };
  422. export type RedPacketModalProps = {
  423. onClose: () => void;
  424. onOpen: (source: any[], index?: number) => void;
  425. };
  426. /**
  427. * @description 红包的三种状态
  428. * is_start 可领取 展示红包领取组件
  429. * is_receive 已领取 展示领取情组件
  430. * is_end 可展示 展示���明页
  431. */
  432. enum Status {
  433. is_start,
  434. is_receive,
  435. is_end,
  436. }
  437. const RedPacketModal = forwardRef<RedPacketModalProps, Props>(function RedPacketModal(props, ref) {
  438. const { onAfterHandler } = props;
  439. const [visible, setVisible] = useState(false);
  440. const [iconLists, setIconLists] = useState<any>([]);
  441. // 初始状态为is_end, 展示活动详情页
  442. const [status, setStatus] = useState<Status>(Status.is_end);
  443. const packetCurrent = useRef<any>({});
  444. const packets = useRef<any[]>([]);
  445. const [redAmount, setRedAmount] = useState<any>(1);
  446. const activeIndex = useRef<number>(0);
  447. const token = getToken();
  448. const element = useRef<HTMLElement | null>(null);
  449. useImperativeHandle(ref, () => {
  450. return {
  451. onClose: () => setVisible(false),
  452. onOpen: (source, index?: number) => {
  453. packets.current = source;
  454. if (index !== null && index !== undefined) {
  455. activeIndex.current = index;
  456. }
  457. getRedPacketInfo().then((res) => {
  458. setVisible(true);
  459. });
  460. },
  461. };
  462. });
  463. const getRedPacketInfo = async () => {
  464. try {
  465. let actList = packets.current;
  466. // 是否开始但是没领过的红包
  467. let packetsFilter = actList
  468. .filter((aItem: any) => {
  469. return aItem.can_receive && aItem.is_start && !aItem.is_receive;
  470. })
  471. .sort((pre: any, next: any) => pre.end_time - next.end_time);
  472. // 有可领取红包
  473. if (packetsFilter.length > 0) {
  474. let current = packetsFilter[activeIndex.current];
  475. let iconList = JSON.parse(current.icon);
  476. // 红包
  477. packetCurrent.current = current;
  478. // 有可领取红包
  479. setIconLists(iconList);
  480. if (!token) {
  481. setStatus(Status.is_end);
  482. } else {
  483. setStatus(Status.is_start);
  484. }
  485. } else {
  486. // 无可领取红包
  487. // 展示最近可领红包详情
  488. let packets = actList.sort((pre: any, next: any) => pre.end_time - next.end_time);
  489. packetCurrent.current = packets[0];
  490. setIconLists(JSON.parse(packets[0].icon));
  491. // 无可领取红包展示详情
  492. setStatus(Status.is_end);
  493. }
  494. } catch (error) {
  495. console.log("redPacketInfo===>error:", error);
  496. }
  497. };
  498. const onReciveRed = async () => {
  499. try {
  500. let paramsData = {
  501. id: packetCurrent.current?.id,
  502. index: packetCurrent.current?.index,
  503. };
  504. let receiveRedPacketInfo = await receiveRedPacketApi(paramsData);
  505. let redNum = receiveRedPacketInfo.data;
  506. if (onAfterHandler) {
  507. onAfterHandler();
  508. }
  509. setStatus(Status.is_receive);
  510. setRedAmount(redNum?.amount);
  511. } catch (error) {
  512. console.log("redPacketInfo===>error:", error);
  513. }
  514. };
  515. useEffect(() => {
  516. element.current = document.getElementById("app");
  517. }, []);
  518. return (
  519. <Mask visible={visible} destroyOnClose={true} getContainer={element.current}>
  520. <FallAnimation1 onClose={() => setVisible(false)} />
  521. {/*<FallAnimation1 onClose={() => setVisible(false)} />*/}
  522. {status === Status.is_start ? (
  523. <HbyInfo
  524. onCloseHby={() => setVisible(false)}
  525. onReciveRed={onReciveRed}
  526. iconImg={iconLists[1]}
  527. />
  528. ) : status === Status.is_receive ? (
  529. <HbyInfo2
  530. onCloseHby={() => setVisible(false)}
  531. redAmount={redAmount}
  532. iconImg={iconLists[2]}
  533. />
  534. ) : (
  535. <HbyInfoDetail onCloseHby={() => setVisible(false)} iconImg={iconLists[0]} />
  536. )}
  537. </Mask>
  538. );
  539. });
  540. export default memo(RedPacketModal);