Selaa lähdekoodia

fix: 红包雨动画重写

Before 7 kuukautta sitten
vanhempi
commit
67f8d840f3

+ 1 - 0
next.config.mjs

@@ -11,6 +11,7 @@ const nextConfig = {
     },
     transpilePackages: ["antd-mobile"],
     sassOptions: {
+        quietDeps: true,
         prependData: `@import "./src/styles/variables.scss";`,
     },
     logging: {

+ 191 - 335
src/components/Box/RedPacketModal.tsx

@@ -6,18 +6,9 @@ import { getToken } from "@/utils/Cookies";
 import { Mask } from "antd-mobile";
 import clsx from "clsx";
 import { useTranslations } from "next-intl";
-import Image from "next/image";
-import {
-    forwardRef,
-    Fragment,
-    memo,
-    useEffect,
-    useImperativeHandle,
-    useRef,
-    useState,
-} from "react";
+import { FC, forwardRef, memo, useEffect, useImperativeHandle, useRef, useState } from "react";
+import animation from "./animations.module.scss";
 import styles from "./redpacked.module.scss";
-
 const randomX = (len: number) => {
     return Math.floor(Math.random() * len);
 };
@@ -46,289 +37,6 @@ function getRandom(min: number, max: number) {
 type DescProps = {
     onClose: () => void;
 };
-const Desc = (props: DescProps) => {
-    const { onClose } = props;
-    const [activeTab, setActiveTab] = useState(0);
-    const tabs = [{ text: "Vezes de evento participado" }, { text: "Consulta de registo levado" }];
-    return (
-        <div className={"absolute top-[30%] w-[100%]"}>
-            <div className={"absolute -top-[1.3rem] left-1/2 w-[3.3rem] -translate-x-1/2"}>
-                <img src="/9f/red-header.png" alt="" />
-                <div
-                    className={
-                        "h-[0.2rem] w-[0.2rem] " + " absolute bottom-[20px] right-[30px]" + " "
-                    }
-                    onClick={onClose}
-                >
-                    <Image src={"/9f/close.png"} alt={"close"} width={25} height={25} />
-                </div>
-            </div>
-            <div
-                className={"mx-auto w-[2.9rem] bg-[#ff9417] px-[0.16rem] pb-[0.12rem] pt-[0.44rem]"}
-            >
-                <img src="/9f/red-title.png" alt="" className={"mx-auto w-[90%]"} />
-                <div className={"mt-[0.0694rem] rounded-[3px] bg-primary-color"}>
-                    <div className={"flex h-[0.54rem] justify-between text-center text-[0.15rem]"}>
-                        {tabs.map((item, index) => {
-                            return (
-                                <Fragment key={index}>
-                                    <span
-                                        onClick={() => setActiveTab(index)}
-                                        className={`flex h-[100%] items-center ${index === activeTab ? "bg-[#8b3500] text-[#5f2600]" : ""}`}
-                                    >
-                                        {item.text}
-                                    </span>
-                                </Fragment>
-                            );
-                        })}
-                    </div>
-                    {/*  动态内容 */}
-                    <div className={"h-[3rem] overflow-y-scroll p-[0.1rem]"}>
-                        <div
-                            className={
-                                "flex items-center rounded-[0.1rem] bg-[#d45300] p-[10px] font-bold"
-                            }
-                        >
-                            <Image
-                                className={"h-[0.43rem] w-[0.7rem]"}
-                                width={80}
-                                height={40}
-                                src="/9f/wallet.png"
-                                alt=""
-                            />
-                            <div className={"text-center"}>
-                                <h2> Tempo regressivo </h2>
-                                <p className={"text-[#fe0]"}>Começa amanhã às 11:00</p>
-                            </div>
-                        </div>
-
-                        {!activeTab ? (
-                            <>
-                                <Image
-                                    src={"/9f/6xtime.png"}
-                                    className={"mt-[0.1rem] h-[0.7rem] w-[100%] rounded-[0.1rem]"}
-                                    width={600}
-                                    height={100}
-                                    alt={"time"}
-                                ></Image>
-
-                                <Image
-                                    src={"/9f/3xtime.png"}
-                                    className={"mt-[0.1rem] h-[0.55rem] w-[100%] rounded-[0.1rem]"}
-                                    width={600}
-                                    height={80}
-                                    alt={"time"}
-                                ></Image>
-                            </>
-                        ) : (
-                            <>
-                                <div
-                                    className={"mt-[0.1rem] rounded-[0.1rem] bg-[#d45300] p-[10px]"}
-                                >
-                                    <p className={"text-[#fe0]"}>Registro persoal</p>
-                                    <div
-                                        className={
-                                            "grid grid-cols-3 text-center" +
-                                            " items-center text-[0.1rem]"
-                                        }
-                                    >
-                                        <span>Nome do jogo</span>
-                                        <span>Coleta cumulativa</span>
-                                        <span>Participação total</span>
-                                    </div>
-
-                                    <div
-                                        className={
-                                            "grid grid-cols-3 text-center" +
-                                            " items-center text-[0.12rem] font-bold"
-                                        }
-                                    >
-                                        <span>55****26</span>
-                                        <span>R$ 100.00</span>
-                                        <span>1</span>
-                                    </div>
-                                </div>
-                                <div
-                                    className={"mt-[0.1rem] rounded-[0.1rem] bg-[#d45300] p-[10px]"}
-                                >
-                                    <p className={"text-[#fe0]"}>Lista dos vencedores</p>
-                                    <div
-                                        className={
-                                            "grid grid-cols-3 text-center" +
-                                            " items-center text-[0.1rem]"
-                                        }
-                                    >
-                                        <span>ID do papel</span>
-                                        <span>Valor obtido</span>
-                                        <span>Hora obtida</span>
-                                    </div>
-                                    <div className={`h-[1rem] overflow-hidden`}>
-                                        {mockData.map((item, index) => {
-                                            return (
-                                                <div
-                                                    key={index}
-                                                    className={`grid grid-cols-3 items-center text-center text-[0.12rem] font-bold ${styles.scrollAnimation}`}
-                                                >
-                                                    <span>{item.phone}</span>
-                                                    <span>R$ {item.num}</span>
-                                                    <span>{item.time}</span>
-                                                </div>
-                                            );
-                                        })}
-                                    </div>
-                                </div>
-                            </>
-                        )}
-
-                        <ul className={"mt-[0.1rem] text-[0.1rem]"}>
-                            <li>
-                                ·Cada sessão de chuva de dinheiro é distribuída gratuitamente por
-                                R$100.000.
-                            </li>
-                            <li>
-                                ·Valor máximo de queda em dinheiro: Cada sessão de chuva de dinheiro
-                                é distribuída gratuitamente.
-                            </li>
-                            <li>·Membros recarregados podem reivindicar gratuitamente.</li>
-                            <li>
-                                ·O dinheiro recebido pode ser utilizado para jogar ou sacar
-                                diretamente.
-                            </li>
-                            <li>
-                                ·Quanto for maior o nível de associação VIP, será maior o valor
-                                recebido.
-                            </li>
-                        </ul>
-                    </div>
-                </div>
-            </div>
-        </div>
-    );
-};
-
-// 红包掉落动画像下雨
-const FallAnimation1 = (props: any) => {
-    const { onClose } = props;
-    const fallContentRef = useRef<HTMLDivElement>(null);
-    const isActive = useRef(true);
-
-    const getRandom = (min: number, max: number) => {
-        return Math.random() * (max - min) + min;
-    };
-
-    const createMoneyElement = (xPos: number) => {
-        if (!fallContentRef.current || !isActive.current) return;
-
-        const money = document.createElement('div');
-        
-        // 基础样式
-        money.style.cssText = `
-            position: fixed;
-            left: ${xPos}px;
-            top: -100px;
-            width: 60px;
-            height: 60px;
-            pointer-events: none;
-        `;
-
-        // 创建图片
-        const img = document.createElement('img');
-        img.src = `/9f/money${Math.floor(Math.random() * 3) + 1}.png`;
-        img.style.cssText = `
-            width: 100%;
-            height: 100%;
-            object-fit: contain;
-        `;
-        
-        money.appendChild(img);
-        fallContentRef.current.appendChild(money);
-
-        // 使用 requestAnimationFrame 确保元素已添加到 DOM 后再添加动画
-        requestAnimationFrame(() => {
-            const scale = getRandom(0.4, 1);
-            const duration = getRandom(8, 12);
-            const delay = getRandom(0, 5);
-            
-            money.style.cssText += `
-                transform: scale(${scale});
-                z-index: ${Math.floor(scale * 100)};
-                animation: ${styles.fall} ${duration}s linear infinite,
-                          ${styles.sway} ${duration/2}s ease-in-out infinite alternate;
-                animation-delay: ${delay}s;
-            `;
-        });
-
-        // 当动画完成一次循环后,重新设置位置
-        setInterval(() => {
-            if (isActive.current) {
-                money.style.left = `${getRandom(0, window.innerWidth)}px`;
-            }
-        }, 12000);
-    };
-
-    useEffect(() => {
-        console.log('FallAnimation1 mounted'); // 调试日志
-
-        if (!fallContentRef.current) return;
-
-        // 设置容器样式
-        fallContentRef.current.style.cssText = `
-            position: fixed;
-            top: 0;
-            left: 0;
-            width: 100%;
-            height: 100vh;
-            z-index: 1000;
-            pointer-events: none;
-            overflow: hidden;
-            background: transparent;
-        `;
-
-        // 分批创建元素
-        const totalElements = 300;
-        const batchSize = 20;
-        let created = 0;
-
-        const createBatch = () => {
-            if (!isActive.current) return;
-
-            for (let i = 0; i < batchSize && created < totalElements; i++) {
-                const xPos = getRandom(0, window.innerWidth);
-                createMoneyElement(xPos);
-                created++;
-            }
-
-            if (created < totalElements) {
-                setTimeout(createBatch, 200);
-            }
-        };
-
-        createBatch();
-
-        return () => {
-            console.log('FallAnimation1 unmounting'); // 调试日志
-            isActive.current = false;
-        };
-    }, []);
-
-    return (
-        <div
-            ref={fallContentRef}
-            style={{
-                position: 'fixed',
-                top: 0,
-                left: 0,
-                width: '100%',
-                height: '100vh',
-                zIndex: 1000,
-            }}
-            onClick={(e) => {
-                e.stopPropagation();
-                onClose?.();
-            }}
-        />
-    );
-};
 
 const HbyInfoDetail = (props: any) => {
     const { iconImg, onCloseHby } = props;
@@ -344,43 +52,8 @@ const HbyInfoDetail = (props: any) => {
         <div
             className={`absolute left-1/2 top-[50%] w-[90%] -translate-x-1/2 -translate-y-1/2 ${styles.promoRules}`}
         >
-            {/* <Image src={"/hby/close.png"} alt={"close"} width={25} height={25} onClick={onCloseHby} className={styles.closeIcon}/> */}
             <div onClick={onCloseHby} className={styles.closeIcon}></div>
-            <Image src={iconImg} onClick={handler} alt={"detail"} width={672} height={1044} />
-            {/* <div className={`h-[0.15rem] text-[#ffd800] text-[0.20rem] text-center ${styles.promoTitle}`}>Dinheiro como chuva</div>
-      <div className={styles.titleWrap}>
-          <span>R$200.00</span>
-          <span> por vez, </span>
-          <span>Máx queda </span>
-          <span>R$7.777</span>
-      </div>
-      <div className={styles.tips}>
-        <img src="/hby/tip-icon.png" alt="tips"  className={styles.tipsIcon}/>
-        <div className={styles.tipsTime}>Começa às 23:00</div>
-      </div>
-      <div className={styles.times1}>
-        <img src="/hby/time1.png"/>
-      </div>
-      <div className={styles.times2}>
-        <img src="/hby/time2.png"/>
-      </div>
-      <ul className={styles.rulelist}>
-        <li className={styles.ruleItem}>
-          Cada sessão de chuva de dinheiro é distribuída gratuitamente com <span>R$200.000</span>
-        </li>
-        <li className={styles.ruleItem}>
-          Valor máximo de queda em dinheiro:Cada sessäo de chuva de dinheiro é distribuida gratuitamente com
-        </li>
-        <li className={styles.ruleItem}>
-          Membros recarregados podem reivindicar gratuitamente
-        </li>
-        <li className={styles.ruleItem}>
-          O dinheiro recebido pode ser utilizado para jogar ou sacado diretamente
-        </li>
-        <li className={styles.ruleItem}>
-          Quanto maior o nivel de associacäo VP, maior o valor recebido
-        </li>
-      </ul> */}
+            <img src={iconImg} onClick={handler} alt={"detail"} width={672} height={1044} />
         </div>
     );
 };
@@ -432,7 +105,7 @@ const HbyInfo2 = (props: any) => {
             className={hbyInfoClass}
             style={{ background: `url(${iconImg})`, backgroundSize: "100% 100%" }}
         >
-            <Image
+            <img
                 src={"/hby/close.png"}
                 alt={"close"}
                 width={30}
@@ -450,6 +123,75 @@ const HbyInfo2 = (props: any) => {
     );
 };
 
+/**
+ * @description  动画背景 - 下雨动效
+ */
+
+const FallAnimation = (props: any) => {
+    const { onClose } = props;
+    const fallContentRef = useRef<HTMLDivElement>(null);
+
+    const totalPackets = 200;
+    const timer = useRef<NodeJS.Timer>(null);
+    const total = useRef(0);
+    const createPacket = (xPoint: number) => {
+        const packetWrapperEl = document.createElement("div");
+        packetWrapperEl.classList.add(styles.packetWrapper);
+        packetWrapperEl.style.left = xPoint + "px";
+        packetWrapperEl.style.width = `77px`;
+        packetWrapperEl.style.height = `44px`;
+        packetWrapperEl.style.transform = `scale(${getRandom(0.4, 1.2)}) `;
+
+        const packetEl = document.createElement("img");
+        packetEl.classList.add(styles.packet);
+
+        packetEl.style.width = `${getRandom(44, 77)}`;
+        packetEl.style.height = `${getRandom(44, 77)}`;
+
+        packetEl.src = `/9f/money${Math.floor(Math.random() * 3) + 1}.png`;
+
+        setInterval(() => {
+            packetEl.style.transform = `rotate(${getRandom(0, 180)}deg) translateX(${Math.random() > 0.5 ? getRandom(0, 180) : -getRandom(0, 180)}px) `;
+        }, 1000);
+
+        packetWrapperEl.appendChild(packetEl);
+        fallContentRef.current?.appendChild(packetWrapperEl);
+    };
+
+    useEffect(() => {
+        // @ts-ignore
+        timer.current = setInterval(() => {
+            if (total.current >= totalPackets - 1) {
+                clearInterval(Number(timer.current));
+                return;
+            }
+            total.current += 1;
+            createPacket(Math.random() * fallContentRef.current?.clientWidth! || 500);
+        }, 200);
+        return () => {
+            clearInterval(Number(timer.current));
+        };
+    }, []);
+    return <div ref={fallContentRef} className={"h-dvh overflow-hidden"} onClick={onClose}></div>;
+};
+const FallAnimation1 = ({ onClose }: { onClose: () => void }) => {
+    const fallContentRef = useRef<HTMLDivElement | null>(null);
+    return (
+        <div
+            ref={fallContentRef}
+            className={"h-[100dvh] h-[100vh] overflow-hidden"}
+            onClick={onClose}
+        >
+            {Array(300)
+                .fill(0)
+                .map((n, index) => (
+                    <div key={index} className={animation.snow}>
+                        <img src={`/9f/money${Math.floor(Math.random() * 3) + 1}.png`} alt="" />
+                    </div>
+                ))}
+        </div>
+    );
+};
 type Props = {
     onAfterHandler?: () => void;
 };
@@ -461,15 +203,125 @@ export type RedPacketModalProps = {
 /**
  * @description 红包的三种状态
  * is_start 可领取 展示红包领取组件
- * is_receive 已领取 展示领取情组件
- * is_end 可展示 展示���明页
+ * is_receive 已领取 展示领取情组件
+ * is_end 可展示 展示明页
  */
 enum Status {
     is_start,
     is_receive,
     is_end,
 }
+interface Snowflake {
+    x: number; // 水平位置
+    y: number; // 垂直位置
+    scale: number; // 缩放比例
+    speedX: number; // 水平移动速度
+    speedY: number; // 垂直下落速度
+    rotate: number; // 当前旋转角度
+    rotateSpeed: number; // 旋转速度
+    image: HTMLImageElement; // 图片路径
+}
 
+interface SnowfallProps {
+    images: string[]; // 图片数组
+    snowflakeCount?: number; // 雪花数量
+    onClose: () => void;
+}
+
+const Snowfall: FC<SnowfallProps> = ({ images, snowflakeCount = 200, onClose = () => {} }) => {
+    //     canvas
+    const canvasRef = useRef<HTMLCanvasElement | null>(null);
+    // 预加载图片
+    const imageElements = useRef<HTMLImageElement[]>([]);
+    // 父元素
+    const containerRef = useRef<HTMLDivElement | null>(null);
+
+    const createSnowflakes = (count: number, width: number, height: number): Snowflake[] => {
+        return Array.from({ length: count }, () => ({
+            x: Math.random() * width,
+            y: Math.random() * height - height,
+            scale: Math.random() * (1.2 - 0.3) + 0.3,
+            speedX: Math.random() * 1.5 - 0.75,
+            speedY: Math.random() * 3 + 1,
+            rotate: Math.random() * 180,
+            rotateSpeed: Math.random() * 2 - 1,
+            image: imageElements.current[Math.floor(Math.random() * imageElements.current.length)],
+        }));
+    };
+    useEffect(() => {
+        // Preload images
+        imageElements.current = images.map((src) => {
+            const img = new Image();
+            img.src = src;
+            return img;
+        });
+
+        const canvas = canvasRef.current;
+        if (!canvas) return;
+
+        const ctx = canvas.getContext("2d");
+        if (!ctx) return;
+
+        const width = (canvas.width = containerRef.current?.clientWidth || 0);
+        const height = (canvas.height = containerRef.current?.clientHeight || 0);
+
+        let snowflakes: Snowflake[] = createSnowflakes(snowflakeCount, width, height);
+
+        const animate = () => {
+            ctx.clearRect(0, 0, width, height);
+            snowflakes.forEach((flake) => {
+                flake.y += flake.speedY;
+                flake.x += flake.speedX;
+                flake.rotate += flake.rotateSpeed;
+
+                if (flake.y > height) {
+                    flake.y = -44;
+                    flake.x = Math.random() * width;
+                    flake.rotate = Math.random() * 360;
+                }
+                if (flake.x < 0 || flake.x > width) {
+                    flake.speedX *= -1;
+                }
+
+                ctx.save();
+                ctx.globalAlpha = 1;
+                ctx.translate(flake.x + (77 * flake.scale) / 2, flake.y + (44 * flake.scale) / 2);
+                ctx.rotate((flake.rotate * Math.PI) / 180);
+                ctx.drawImage(
+                    flake.image,
+                    (-77 * flake.scale) / 2,
+                    (-44 * flake.scale) / 2,
+                    77 * flake.scale,
+                    44 * flake.scale
+                );
+                ctx.restore();
+            });
+
+            requestAnimationFrame(animate);
+        };
+
+        animate();
+
+        const handleResize = () => {
+            canvas.width = containerRef.current?.clientWidth || 0;
+            console.dir(containerRef.current);
+            canvas.height = containerRef.current?.clientHeight || 0;
+            snowflakes = createSnowflakes(snowflakeCount, canvas.width, canvas.height);
+        };
+
+        window.addEventListener("resize", handleResize);
+
+        return () => {
+            window.removeEventListener("resize", handleResize);
+        };
+    }, []);
+
+    return (
+        <div className={"absolute h-[100%] w-[100%]"} ref={containerRef} onClick={onClose}>
+            <canvas ref={canvasRef} style={{ display: "block" }} />;
+        </div>
+    );
+};
 const RedPacketModal = forwardRef<RedPacketModalProps, Props>(function RedPacketModal(props, ref) {
     const { onAfterHandler } = props;
     const [visible, setVisible] = useState(false);
@@ -502,7 +354,7 @@ const RedPacketModal = forwardRef<RedPacketModalProps, Props>(function RedPacket
     const getRedPacketInfo = async () => {
         try {
             let actList = packets.current;
-            // 是否开始但是没领过的红包
+            // 是否有已开始但是没领过的红包
             let packetsFilter = actList
                 .filter((aItem: any) => {
                     return aItem.can_receive && aItem.is_start && !aItem.is_receive;
@@ -559,9 +411,13 @@ const RedPacketModal = forwardRef<RedPacketModalProps, Props>(function RedPacket
     }, []);
     return (
         <Mask visible={visible} destroyOnClose={true} getContainer={element.current}>
-            <FallAnimation1 onClose={() => setVisible(false)} />
             {/*<FallAnimation1 onClose={() => setVisible(false)} />*/}
 
+            <Snowfall
+                images={["/9f/money1.png", "/9f/money2.png", "/9f/money3.png"]}
+                onClose={() => setVisible(false)}
+            />
+
             {status === Status.is_start ? (
                 <HbyInfo
                     onCloseHby={() => setVisible(false)}

+ 45 - 0
src/components/Box/animations.module.scss

@@ -148,3 +148,48 @@ $scaleTime: 3s;
     background-position: -100% 0;  /* 终点: /* 终点:向左完全滚出容器 */
   }
 }
+
+/// 红包雨摇曳动画
+@function random_range($min, $max) {
+  $rand: random();
+  $random_range: $min + floor($rand * (($max - $min) + 1));
+  @return $random_range;
+}
+
+.snow {
+  $total: 300;
+  position: absolute;
+  //width: 77px;
+  //height: 44px;
+  width: fit-content;
+  height: fit-content;
+
+  @for $i from 1 through $total {
+    $random-x: random(100) * 1vw;
+    $random-offset: random_range(-20, 20) * 1vw;
+    $random-x-end: $random-x + $random-offset;
+    $random-x-end-yoyo: $random-x + ($random-offset / 2);
+    $random-yoyo-time: random_range(0.3, 0.8);
+    $random-yoyo-y: $random-yoyo-time * 100vh;
+    $random-scale: random_range(3000, 10000) * 0.0001;
+    $fall-duration: random_range(10, 30) * .5s;
+    $fall-delay: random(30) * -1s;
+    $rotate-speed: random_range(360, 1440);
+    @debug $random-offset;
+    &:nth-child(#{$i}) {
+      //opacity: random(10000) * 0.0001;
+      transform: translate($random-x, -20px) scale($random-scale)  rotate(180deg);
+      animation: fall-#{$i} $fall-duration $fall-delay linear infinite;
+    }
+
+    @keyframes fall-#{$i} {
+      #{percentage($random-yoyo-time)} {
+        transform: translate($random-x-end, $random-yoyo-y) scale($random-scale);
+      }
+
+      to {
+        transform: translate($random-x-end-yoyo, 100vh) scale($random-scale)  rotate(180deg) ;
+      }
+    }
+  }
+}

+ 24 - 20
src/components/Box/redpacked.module.scss

@@ -1,22 +1,11 @@
-@keyframes fall {
-    0% {
-        transform: translateY(-100px);
-    }
-    100% {
-        transform: translateY(120vh);
-    }
-}
+@keyframes smoothscroll {
+  0% {
+    transform: translateY(0);
+  }
 
-@keyframes sway {
-    0% {
-        transform: translateX(-100px) rotate(-20deg);
-    }
-    50% {
-        transform: translateX(100px) rotate(20deg);
-    }
-    100% {
-        transform: translateX(-100px) rotate(-20deg);
-    }
+  100% {
+    transform: translateY(-20rem);
+  }
 }
 
 .scrollAnimation {
@@ -77,8 +66,12 @@
 }
 
 .packetWrapper {
-  will-change: transform;
-  pointer-events: none;
+  position: absolute;
+  top: 0;
+  left: 0;
+  transform: translateY(-100%);
+  animation: down 8s linear infinite;
+  font-size: 0;
 }
 
 .packet{
@@ -89,6 +82,17 @@
   transform: rotate(0);
 }
 
+@keyframes down {
+  0% {
+    transform: translateY(-100%);
+  }
+
+  100% {
+    transform: translateY(100vh);
+  }
+}
+
+
 .promoRules{
   // background:url('/hby/hby_bg.png') no-repeat;
   background-size: 100% 100%;