index.tsx 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409
  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 { FC, forwardRef, memo, useEffect, useImperativeHandle, useRef, useState } from "react";
  9. import styles from "./style.module.scss";
  10. const randomX = (len: number) => {
  11. return Math.floor(Math.random() * len);
  12. };
  13. const mockData = Array(500)
  14. .fill(0)
  15. .map((item) => {
  16. return {
  17. phone: `55****${(randomX(99) + "").padEnd(2, "0")}`,
  18. num: `${(Math.random() * 20).toFixed(2)}`,
  19. time: "11:00",
  20. };
  21. });
  22. function getRandom(min: number, max: number) {
  23. const floatRandom = Math.random();
  24. const difference = max - min;
  25. // 介于 0 和差值之间的随机数
  26. const random = Math.round(difference * floatRandom);
  27. return random + min;
  28. }
  29. /**
  30. * @description 描述
  31. */
  32. type DescProps = {
  33. onClose: () => void;
  34. };
  35. const HbyInfoDetail = (props: any) => {
  36. const { iconImg, onCloseHby } = props;
  37. const router = useRouter();
  38. const token = getToken();
  39. const handler = () => {
  40. if (!token) {
  41. router.push("/login");
  42. }
  43. onCloseHby();
  44. };
  45. const str = "12:00".split("");
  46. return (
  47. <div className={`absolute h-[100%] w-[100%]`} onClick={onCloseHby}>
  48. <div className={"flex h-[100%] w-[100%] items-center"}>
  49. <div className={"relative h-[6.2rem] w-[100%]"}>
  50. <img className={"h-[6.2rem] w-[100%]"} src="/redpacket/isEndBg.png" alt="" />
  51. {/* 下一场开始时间*/}
  52. <div className={"absolute top-[27%] w-[100%]"}>
  53. <div className={"relative h-[0.4306rem] border-primary-color"}>
  54. <div className="absolute left-[21%] h-[100%] w-[0.4167rem] text-center text-[0.2778rem] font-black text-[red]">
  55. {str[0]}
  56. </div>
  57. <div className="absolute left-[36%] h-[100%] w-[0.4167rem] text-center text-[0.2778rem] font-black text-[red]">
  58. {str[1]}
  59. </div>
  60. <div className="absolute left-[47%] h-[100%] w-[0.2167rem] text-center text-[0.2778rem] font-black text-[red]">
  61. {str[2]}
  62. </div>
  63. <div className="absolute left-[54%] h-[100%] w-[0.4167rem] text-center text-[0.2778rem] font-black text-[red]">
  64. {str[3]}
  65. </div>
  66. <div className="absolute left-[68%] h-[100%] w-[0.4167rem] text-center text-[0.2778rem] font-black text-[red]">
  67. {str[4]}
  68. </div>
  69. </div>
  70. </div>
  71. <div
  72. className={
  73. "absolute top-[53%] h-[0.6944rem] w-[100%] px-[0.5556rem]" +
  74. " overflow-scroll border-primary-color text-primary-color"
  75. }
  76. ></div>
  77. </div>
  78. </div>
  79. </div>
  80. );
  81. };
  82. const HbyInfo = (props: any) => {
  83. const { iconImg, onCloseHby, onReciveRed } = props;
  84. if (!iconImg) return;
  85. return (
  86. <div
  87. className={`absolute left-1/2 top-[50%] -translate-x-1/2 -translate-y-1/2 ${styles.redclose}`}
  88. >
  89. {/* <Image src={"/hby/close.png"} alt={"close"} width={20} height={20} onClick={onCloseHby} className={styles.closeIcon}/> */}
  90. <div onClick={onCloseHby} className={styles.closeIcon}></div>
  91. <img
  92. src={iconImg}
  93. alt={"icon"}
  94. width={559}
  95. height={687}
  96. onClick={onReciveRed}
  97. className={styles.redIcon}
  98. />
  99. {/* <div className={styles.title}>Chuva de dinheiro</div>
  100. <div className={styles.desc}>
  101. <ul className={styles.desclist}>
  102. <li className={styles.descitem}> Membros recarregados podem reivindicar gratuitamente. </li>
  103. <div className={styles.line}></div>
  104. <li className={styles.descitem}> Valor máximo de queda em dinheiro: R$7.777 </li>
  105. </ul>
  106. </div>
  107. <div className={styles.openBtn} onClick={onReciveRed}>AGARRAR</div> */}
  108. </div>
  109. // <div data-v-f333135e="" className={`absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 ${styles.redopen}`}>
  110. // <Image src={"/hby/close.png"} alt={"close"} width={20} height={20} onClick={onCloseHby} className={styles.closeIcon}/>
  111. // <div className={styles.title}>Chuva de dinheiro</div>
  112. // <div className={styles.cash}>1.96</div>
  113. // <div className={styles.tips}>Valor máximo de queda em dinheiro:Cada sessäo de chuva de dinheiro é </div>
  114. // </div>
  115. );
  116. };
  117. const HbyInfo2 = (props: any) => {
  118. const { iconImg, onCloseHby, redAmount } = props;
  119. const hbyInfoClass = clsx(
  120. `${styles.redopen} absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 bg-no-repeat bg-cover`
  121. );
  122. const t = useTranslations("packetsPopup");
  123. return (
  124. <div
  125. className={hbyInfoClass}
  126. style={{ background: `url(${iconImg})`, backgroundSize: "100% 100%" }}
  127. >
  128. <img
  129. src={"/hby/close.png"}
  130. alt={"close"}
  131. width={30}
  132. height={30}
  133. onClick={onCloseHby}
  134. className={styles.closeIcon}
  135. />
  136. <div className={styles.title}>{t("title")}</div>
  137. <div className={styles.cash}>{redAmount}</div>
  138. <div className={styles.tips}>
  139. {redAmount > 0 ? t("receiveSuccess") : t("receiveWarring")}
  140. </div>
  141. </div>
  142. );
  143. };
  144. /**
  145. * @description 动画背景 - 下雨动效
  146. */
  147. type Props = {
  148. onAfterHandler?: () => void;
  149. };
  150. export type RedPacketModalProps = {
  151. onClose: () => void;
  152. onOpen: (source: any[], index?: number) => void;
  153. };
  154. /**
  155. * @description 红包的三种状态
  156. * is_start 可领取 展示红包领取组件
  157. * is_receive 已领取 展示领取详情组件
  158. * is_end 可展示 展示说明页
  159. */
  160. enum Status {
  161. is_start,
  162. is_receive,
  163. is_end,
  164. }
  165. interface Snowflake {
  166. x: number; // 水平位置
  167. y: number; // 垂直位置
  168. scale: number; // 缩放比例
  169. speedX: number; // 水平移动速度
  170. speedY: number; // 垂直下落速度
  171. rotate: number; // 当前旋转角度
  172. rotateSpeed: number; // 旋转速度
  173. image: HTMLImageElement; // 图片路径
  174. }
  175. interface SnowfallProps {
  176. images: string[]; // 图片数组
  177. snowflakeCount?: number; // 雪花数量
  178. onClose: () => void;
  179. }
  180. const Snowfall: FC<SnowfallProps> = ({ images, snowflakeCount = 80, onClose = () => {} }) => {
  181. // canvas
  182. const canvasRef = useRef<HTMLCanvasElement | null>(null);
  183. // 预加载图片
  184. const imageElements = useRef<HTMLImageElement[]>([]);
  185. // 父元素
  186. const containerRef = useRef<HTMLDivElement | null>(null);
  187. const createSnowflakes = (count: number, width: number, height: number): Snowflake[] => {
  188. return Array.from({ length: count }, () => ({
  189. x: Math.random() * width,
  190. y: Math.random() * height - height,
  191. scale: Math.random() * (1.2 - 0.5) + 0.5,
  192. speedX: Math.random() * 1.5 - 0.75,
  193. speedY: Math.random() * 5 + 1,
  194. rotate: Math.random() * 180,
  195. rotateSpeed: Math.random() * 2 - 1,
  196. image: imageElements.current[Math.floor(Math.random() * imageElements.current.length)],
  197. }));
  198. };
  199. useEffect(() => {
  200. // 预加载图片
  201. imageElements.current = images.map((src) => {
  202. const img = new Image();
  203. img.src = src;
  204. return img;
  205. });
  206. const canvas = canvasRef.current;
  207. if (!canvas) return;
  208. const ctx = canvas.getContext("2d");
  209. if (!ctx) return;
  210. const parentWidth = containerRef.current?.clientWidth || 0;
  211. const parentHeight = (canvas.height = containerRef.current?.clientHeight || 0);
  212. const width = (canvas.width = parentWidth);
  213. const height = (canvas.height = parentHeight);
  214. let snowflakes: Snowflake[] = createSnowflakes(snowflakeCount, width, height);
  215. const imgHeight = 80;
  216. const imgWidth = 80;
  217. const animate = () => {
  218. ctx.clearRect(0, 0, width, height);
  219. snowflakes.forEach((flake) => {
  220. flake.y += flake.speedY;
  221. flake.x += flake.speedX;
  222. flake.rotate += flake.rotateSpeed;
  223. if (flake.y > height) {
  224. flake.y = -44;
  225. flake.x = Math.random() * width;
  226. flake.rotate = Math.random() * 360;
  227. }
  228. if (flake.x < 0 || flake.x > width) {
  229. flake.speedX *= -1;
  230. }
  231. ctx.save();
  232. ctx.globalAlpha = 1;
  233. ctx.translate(
  234. flake.x + (imgHeight * flake.scale) / 2,
  235. flake.y + (imgWidth * flake.scale) / 2
  236. );
  237. ctx.rotate((flake.rotate * Math.PI) / 180);
  238. ctx.drawImage(
  239. flake.image,
  240. (-imgHeight * flake.scale) / 2,
  241. (-imgWidth * flake.scale) / 2,
  242. imgHeight * flake.scale,
  243. imgWidth * flake.scale
  244. );
  245. ctx.restore();
  246. });
  247. requestAnimationFrame(animate);
  248. };
  249. animate();
  250. const handleResize = () => {
  251. canvas.width = parentWidth;
  252. canvas.height = parentHeight;
  253. snowflakes = createSnowflakes(snowflakeCount, canvas.width, canvas.height);
  254. };
  255. window.addEventListener("resize", handleResize);
  256. return () => {
  257. window.removeEventListener("resize", handleResize);
  258. };
  259. }, []);
  260. return (
  261. <div className={"absolute h-[100%] w-[100%]"} ref={containerRef} onClick={onClose}>
  262. <canvas ref={canvasRef} style={{ display: "block" }} />;
  263. </div>
  264. );
  265. };
  266. const RedPacketModal = forwardRef<RedPacketModalProps, Props>(function RedPacketModal(props, ref) {
  267. const { onAfterHandler } = props;
  268. const [visible, setVisible] = useState(false);
  269. const [iconLists, setIconLists] = useState<any>([]);
  270. // 初始状态为is_end, 展示活动详情页
  271. const [status, setStatus] = useState<Status>(Status.is_end);
  272. const packetCurrent = useRef<any>({});
  273. const packets = useRef<any[]>([]);
  274. const [redAmount, setRedAmount] = useState<any>(1);
  275. const activeIndex = useRef<number>(0);
  276. const token = getToken();
  277. const element = useRef<HTMLElement | null>(null);
  278. useImperativeHandle(ref, () => {
  279. return {
  280. onClose: () => setVisible(false),
  281. onOpen: (source, index?: number) => {
  282. packets.current = source;
  283. if (index !== null && index !== undefined) {
  284. activeIndex.current = index;
  285. }
  286. getRedPacketInfo().then((res) => {
  287. setVisible(true);
  288. });
  289. },
  290. };
  291. });
  292. const getRedPacketInfo = async () => {
  293. try {
  294. let actList = packets.current;
  295. // 是否有已开始但是没领过的红包
  296. let packetsFilter = actList
  297. .filter((aItem: any) => {
  298. return aItem.can_receive && aItem.is_start && !aItem.is_receive;
  299. })
  300. .sort((pre: any, next: any) => pre.end_time - next.end_time);
  301. // 有可领取红包
  302. if (packetsFilter.length > 0) {
  303. let current = packetsFilter[activeIndex.current];
  304. let iconList = JSON.parse(current.icon);
  305. // 红包
  306. packetCurrent.current = current;
  307. // 有可领取红包
  308. setIconLists(iconList);
  309. if (!token) {
  310. setStatus(Status.is_end);
  311. } else {
  312. setStatus(Status.is_start);
  313. }
  314. } else {
  315. // 无可领取红包
  316. // 展示最近可领红包详情
  317. let packets = actList.sort((pre: any, next: any) => pre.end_time - next.end_time);
  318. packetCurrent.current = packets[0];
  319. setIconLists(JSON.parse(packets[0].icon));
  320. // 无可领取红包展示详情
  321. setStatus(Status.is_end);
  322. }
  323. } catch (error) {
  324. console.log("redPacketInfo===>error:", error);
  325. }
  326. };
  327. const onReciveRed = async () => {
  328. try {
  329. let paramsData = {
  330. id: packetCurrent.current?.id,
  331. index: packetCurrent.current?.index,
  332. };
  333. let receiveRedPacketInfo = await receiveRedPacketApi(paramsData);
  334. let redNum = receiveRedPacketInfo.data;
  335. if (onAfterHandler) {
  336. onAfterHandler();
  337. }
  338. setStatus(Status.is_receive);
  339. setRedAmount(redNum?.amount);
  340. } catch (error) {
  341. console.log("redPacketInfo===>error:", error);
  342. }
  343. };
  344. useEffect(() => {
  345. element.current = document.getElementById("app");
  346. }, []);
  347. const ImagesData = Array.from({ length: 6 }).map((_, index) => {
  348. return `/9f/money${index + 1}.png`;
  349. });
  350. return (
  351. <Mask visible={visible} destroyOnClose={true} getContainer={element.current} opacity={0.75}>
  352. <Snowfall images={ImagesData} onClose={() => setVisible(false)} />
  353. {status === Status.is_start ? (
  354. <HbyInfo
  355. onCloseHby={() => setVisible(false)}
  356. onReciveRed={onReciveRed}
  357. iconImg={iconLists[1]}
  358. />
  359. ) : status === Status.is_receive ? (
  360. <HbyInfo2
  361. onCloseHby={() => setVisible(false)}
  362. redAmount={redAmount}
  363. iconImg={iconLists[2]}
  364. />
  365. ) : (
  366. <HbyInfoDetail onCloseHby={() => setVisible(false)} iconImg={iconLists[0]} />
  367. )}
  368. </Mask>
  369. );
  370. });
  371. export default memo(RedPacketModal);