index.tsx 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505
  1. "use client";
  2. import { receiveRedPacketApi } from "@/api/promo";
  3. import { useRouter } from "@/i18n/routing";
  4. import { getToken } from "@/utils/Cookies";
  5. import { Mask, Toast } from "antd-mobile";
  6. import { useTranslations } from "next-intl";
  7. import { FC, forwardRef, memo, useEffect, useImperativeHandle, useRef, useState } from "react";
  8. const randomX = (len: number) => {
  9. return Math.floor(Math.random() * len);
  10. };
  11. const mockData = Array(500)
  12. .fill(0)
  13. .map((item) => {
  14. return {
  15. phone: `55****${(randomX(99) + "").padEnd(2, "0")}`,
  16. num: `${(Math.random() * 20).toFixed(2)}`,
  17. time: "11:00",
  18. };
  19. });
  20. function getRandom(min: number, max: number) {
  21. const floatRandom = Math.random();
  22. const difference = max - min;
  23. // 介于 0 和差值之间的随机数
  24. const random = Math.round(difference * floatRandom);
  25. return random + min;
  26. }
  27. /**
  28. * @description 描述
  29. */
  30. type DescProps = {
  31. onClose: () => void;
  32. };
  33. export interface RedPacketTypes {
  34. countdown: number;
  35. /**
  36. * 活动结束时间戳
  37. */
  38. end_time: number;
  39. /**
  40. * 活动详情图片
  41. */
  42. icon: string;
  43. /**
  44. * 活动ID
  45. */
  46. id: number;
  47. /**
  48. * 红包索引
  49. */
  50. index: number;
  51. /**
  52. * 是否能领取
  53. */
  54. can_receive: boolean;
  55. /**
  56. * 是否领取
  57. */
  58. is_receive: boolean;
  59. /**
  60. * 是否开始
  61. */
  62. is_start: boolean;
  63. /**
  64. * 红包详情
  65. */
  66. rewards: Reward[];
  67. /**
  68. * 活动开始时间戳
  69. */
  70. start_time: number;
  71. /**
  72. * 显示时间
  73. */
  74. time_string: string;
  75. times: string[];
  76. }
  77. export interface Reward {
  78. /**
  79. * 红包等级
  80. */
  81. level: number;
  82. max_amount: number;
  83. max_vip: number;
  84. min_amount: number;
  85. min_vip: number;
  86. multiple: number;
  87. /**
  88. * 红包名称
  89. */
  90. name: string;
  91. total: number;
  92. }
  93. interface PacketDetailsProps {
  94. packet: RedPacketTypes;
  95. iconImg: string;
  96. onCloseHby: () => void;
  97. onReciveRed: () => void;
  98. redAmount: number;
  99. }
  100. /// 红包详情
  101. const PacketEndDetail: FC<Omit<PacketDetailsProps, "onReciveRed" | "redAmount">> = (props) => {
  102. const { iconImg, onCloseHby, packet } = props;
  103. const router = useRouter();
  104. const token = getToken();
  105. const handler = () => {
  106. if (!token) {
  107. router.push("/login");
  108. }
  109. onCloseHby();
  110. };
  111. const str = packet.time_string.split("");
  112. return (
  113. <div className={`absolute h-[100%] w-[100%]`} onClick={handler}>
  114. <div className={"flex h-[100%] w-[100%] items-center"}>
  115. <div className={"relative h-[6.2rem] w-[100%]"}>
  116. <img className={"h-[6.2rem] w-[100%]"} src={iconImg} alt="" />
  117. {/* 下一场开始时间*/}
  118. <div className={"absolute top-[27%] w-[100%]"}>
  119. <div className={"relative h-[0.4306rem] border-primary-color"}>
  120. <div className="absolute left-[21%] h-[100%] w-[0.4167rem] text-center text-[0.2778rem] font-black text-[red]">
  121. {str[0]}
  122. </div>
  123. <div className="absolute left-[36%] h-[100%] w-[0.4167rem] text-center text-[0.2778rem] font-black text-[red]">
  124. {str[1]}
  125. </div>
  126. <div className="absolute left-[47%] h-[100%] w-[0.2167rem] text-center text-[0.2778rem] font-black text-[red]">
  127. {str[2]}
  128. </div>
  129. <div className="absolute left-[54%] h-[100%] w-[0.4167rem] text-center text-[0.2778rem] font-black text-[red]">
  130. {str[3]}
  131. </div>
  132. <div className="absolute left-[68%] h-[100%] w-[0.4167rem] text-center text-[0.2778rem] font-black text-[red]">
  133. {str[4]}
  134. </div>
  135. </div>
  136. </div>
  137. <div
  138. className={
  139. "absolute top-[53%] h-[0.6944rem] w-[100%] px-[0.6944rem]" +
  140. " grid grid-cols-3 overflow-scroll border-primary-color" +
  141. " text-[18px] text-[red]"
  142. }
  143. >
  144. {packet.times.map((time, parentIndex) => {
  145. return (
  146. <div key={parentIndex} className={""}>
  147. {time}
  148. </div>
  149. );
  150. })}
  151. </div>
  152. </div>
  153. </div>
  154. </div>
  155. );
  156. };
  157. const PacketStartDetail: FC<Omit<PacketDetailsProps, "redAmount">> = (props) => {
  158. const { iconImg, onCloseHby, onReciveRed, packet } = props;
  159. const maxCount = packet.rewards
  160. .reduce((p, n) => (p > n.max_amount ? p : n.max_amount), 0)
  161. .toFixed(2);
  162. return (
  163. <div className={`absolute h-[100%] w-[100%]`} onClick={onCloseHby}>
  164. <div className={"flex h-[100%] w-[100%]"}>
  165. <div className={"relative h-[6.25rem] w-[100%] md:mt-[30px]"}>
  166. <img src="/redpacket/isStartBg.png" alt="" className={"h-[6.25rem] w-[100%]"} />
  167. <div
  168. className={
  169. "absolute top-[70%] h-[0.6944rem] w-[100%] px-[0.7rem]" +
  170. " text-center text-[0.16rem] font-bold text-[#019632]" +
  171. " [text-shadow:1px_1px_#fff]"
  172. }
  173. >
  174. <p>Valor máximo de queda em</p>
  175. <p>
  176. dinheiro: <span className={"text-primary-color"}>R${maxCount}</span>
  177. </p>
  178. </div>
  179. <div
  180. className={
  181. "absolute bottom-[10%] left-[50%] h-[65px] w-[250px] " +
  182. " -translate-x-1/2"
  183. }
  184. onClick={(e) => {
  185. e.stopPropagation();
  186. onReciveRed();
  187. }}
  188. ></div>
  189. </div>
  190. </div>
  191. </div>
  192. );
  193. };
  194. const PacketReceiveDetail: FC<Omit<PacketDetailsProps, "onReciveRed" | "packet">> = (props) => {
  195. const { iconImg, onCloseHby, redAmount } = props;
  196. const t = useTranslations("packetsPopup");
  197. return (
  198. <div className={`absolute h-[100%] w-[100%]`} onClick={onCloseHby}>
  199. <div className={"flex h-[100%] w-[100%]"}>
  200. <div className={"relative h-[6.25rem] w-[100%] md:mt-[30px]"}>
  201. <img src={iconImg} alt="" className={"h-[6.25rem] w-[100%]"} />
  202. <div
  203. className={
  204. "absolute top-[28%] w-[100%] text-center font-black" +
  205. " text-[0.4167rem] text-[#1fa62e]"
  206. }
  207. >
  208. <p>R$</p>
  209. <p className={"-mt-[0.0694rem]"}>{redAmount.toFixed(2)}</p>
  210. </div>
  211. <div
  212. className={
  213. "absolute bottom-[15%] h-[1.1rem] px-[0.8333rem]" + " text-[#ff9000]"
  214. }
  215. >
  216. <div className={"text-center text-[0.1667rem] font-bold"}>
  217. {redAmount > 0 ? t("receiveSuccess") : t("receiveWarring")}
  218. </div>
  219. </div>
  220. </div>
  221. </div>
  222. </div>
  223. );
  224. };
  225. /**
  226. * @description 动画背景 - 下雨动效
  227. */
  228. type Props = {
  229. onAfterHandler?: () => void;
  230. };
  231. export type RedPacketModalProps = {
  232. onClose: () => void;
  233. onOpen: (source: any[], index?: number) => void;
  234. };
  235. /**
  236. * @description 红包的三种状态
  237. * is_start 可领取 展示红包领取组件
  238. * is_receive 已领取 展示领取详情组件
  239. * is_end 可展示 展示说明页
  240. */
  241. enum Status {
  242. is_start,
  243. is_receive,
  244. is_end,
  245. }
  246. interface Snowflake {
  247. x: number; // 水平位置
  248. y: number; // 垂直位置
  249. opacity: number; // 透明度
  250. scale: number; // 缩放比例
  251. speedX: number; // 水平移动速度
  252. speedY: number; // 垂直下落速度
  253. rotate: number; // 当前旋转角度
  254. rotateSpeed: number; // 旋转速度
  255. image: HTMLImageElement; // 图片路径
  256. }
  257. interface SnowfallProps {
  258. images: string[]; // 图片数组
  259. snowflakeCount?: number; // 雪花数量
  260. onClose: () => void;
  261. }
  262. const Snowfall: FC<SnowfallProps> = ({ images, snowflakeCount = 80, onClose = () => {} }) => {
  263. // canvas
  264. const canvasRef = useRef<HTMLCanvasElement | null>(null);
  265. // 预加载图片
  266. const imageElements = useRef<HTMLImageElement[]>([]);
  267. // 父元素
  268. const containerRef = useRef<HTMLDivElement | null>(null);
  269. const createSnowflakes = (count: number, width: number, height: number): Snowflake[] => {
  270. return Array.from({ length: count }, () => ({
  271. x: Math.random() * width,
  272. y: Math.random() * height - height,
  273. scale: Math.random() * (1.2 - 0.5) + 0.5,
  274. speedX: Math.random() * 1.5 - 0.75,
  275. speedY: Math.random() * 5 + 1,
  276. rotate: Math.random() * 180,
  277. rotateSpeed: Math.random() * 2 - 1,
  278. opacity: Math.random() * (1.2 - 0.5) + 0.5,
  279. image: imageElements.current[Math.floor(Math.random() * imageElements.current.length)],
  280. }));
  281. };
  282. useEffect(() => {
  283. // 预加载图片
  284. imageElements.current = images.map((src) => {
  285. const img = new Image();
  286. img.src = src;
  287. return img;
  288. });
  289. const canvas = canvasRef.current;
  290. if (!canvas) return;
  291. const ctx = canvas.getContext("2d");
  292. if (!ctx) return;
  293. const parentWidth = containerRef.current?.clientWidth || 0;
  294. const parentHeight = (canvas.height = containerRef.current?.clientHeight || 0);
  295. const width = (canvas.width = parentWidth);
  296. const height = (canvas.height = parentHeight);
  297. let snowflakes: Snowflake[] = createSnowflakes(snowflakeCount, width, height);
  298. const imgHeight = 80;
  299. const imgWidth = 80;
  300. const animate = () => {
  301. ctx.clearRect(0, 0, width, height);
  302. snowflakes.forEach((flake) => {
  303. flake.y += flake.speedY;
  304. flake.x += flake.speedX;
  305. flake.rotate += flake.rotateSpeed;
  306. if (flake.y > height) {
  307. flake.y = -44;
  308. flake.x = Math.random() * width;
  309. flake.rotate = Math.random() * 360;
  310. }
  311. if (flake.x < 0 || flake.x > width) {
  312. flake.speedX *= -1;
  313. }
  314. ctx.save();
  315. ctx.globalAlpha = flake.opacity;
  316. ctx.translate(
  317. flake.x + (imgHeight * flake.scale) / 2,
  318. flake.y + (imgWidth * flake.scale) / 2
  319. );
  320. ctx.rotate((flake.rotate * Math.PI) / 180);
  321. ctx.drawImage(
  322. flake.image,
  323. (-imgHeight * flake.scale) / 2,
  324. (-imgWidth * flake.scale) / 2,
  325. imgHeight * flake.scale,
  326. imgWidth * flake.scale
  327. );
  328. ctx.restore();
  329. });
  330. requestAnimationFrame(animate);
  331. };
  332. animate();
  333. const handleResize = () => {
  334. canvas.width = parentWidth;
  335. canvas.height = parentHeight;
  336. snowflakes = createSnowflakes(snowflakeCount, canvas.width, canvas.height);
  337. };
  338. window.addEventListener("resize", handleResize);
  339. return () => {
  340. window.removeEventListener("resize", handleResize);
  341. };
  342. }, []);
  343. return (
  344. <div className={"absolute h-[100%] w-[100%]"} ref={containerRef} onClick={onClose}>
  345. <canvas ref={canvasRef} style={{ display: "block" }} />;
  346. </div>
  347. );
  348. };
  349. const RedPacketModal = forwardRef<RedPacketModalProps, Props>(function RedPacketModal(props, ref) {
  350. const { onAfterHandler } = props;
  351. const [visible, setVisible] = useState(false);
  352. const [iconLists, setIconLists] = useState<string[]>([]);
  353. // 初始状态为is_end, 展示活动详情页
  354. const [status, setStatus] = useState<Status>(Status.is_end);
  355. const packetCurrent = useRef<RedPacketTypes | null>(null);
  356. const packets = useRef<RedPacketTypes[]>([]);
  357. const [redAmount, setRedAmount] = useState<number>(1);
  358. const activeIndex = useRef<number>(0);
  359. const token = getToken();
  360. const element = useRef<HTMLElement | null>(null);
  361. useImperativeHandle(ref, () => {
  362. return {
  363. onClose: () => setVisible(false),
  364. onOpen: (source, index?: number) => {
  365. packets.current = source;
  366. if (index !== null && index !== undefined) {
  367. activeIndex.current = index;
  368. }
  369. getRedPacketInfo().then((res) => {
  370. setVisible(true);
  371. });
  372. },
  373. };
  374. });
  375. const getRedPacketInfo = async () => {
  376. try {
  377. let actList = packets.current;
  378. // 是否有已开始但是没领过的红包
  379. let packetsFilter = actList;
  380. // .filter((aItem) => {
  381. // return aItem.is_start && aItem.can_receive && !aItem.is_receive;
  382. // })
  383. // .sort((pre, next) => pre.end_time - next.end_time);
  384. // 有可领取红包 - is_start = true -> all
  385. if (packetsFilter.length > 0) {
  386. let current = packetsFilter[activeIndex.current];
  387. let iconList = JSON.parse(current.icon);
  388. // 红包
  389. packetCurrent.current = current;
  390. // 有可领取红包
  391. setIconLists(iconList);
  392. if (!token) {
  393. setStatus(Status.is_end);
  394. } else {
  395. setStatus(
  396. current.is_start && current.can_receive ? Status.is_start : Status.is_end
  397. );
  398. }
  399. } else {
  400. // 无可领取红包
  401. // 展示最近可领红包详情
  402. // let packets = actList.sort((pre, next) => pre.end_time - next.end_time);
  403. // packetCurrent.current = packets[0];
  404. // setIconLists(JSON.parse(packets[0].icon));
  405. // // 无可领取红包展示详情
  406. // setStatus(Status.is_end);
  407. Toast.show("The event is not open");
  408. }
  409. } catch (error) {
  410. console.log("redPacketInfo===>error:", error);
  411. }
  412. };
  413. const onReciveRed = async () => {
  414. try {
  415. let paramsData = {
  416. id: packetCurrent.current?.id!,
  417. index: packetCurrent.current?.index!,
  418. };
  419. let receiveRedPacketInfo = await receiveRedPacketApi(paramsData);
  420. let redNum = receiveRedPacketInfo.data;
  421. if (onAfterHandler) {
  422. onAfterHandler();
  423. }
  424. setStatus(Status.is_receive);
  425. setRedAmount(redNum?.amount);
  426. } catch (error) {
  427. console.log("redPacketInfo===>error:", error);
  428. }
  429. };
  430. useEffect(() => {
  431. element.current = document.getElementById("app");
  432. }, []);
  433. const ImagesData = Array.from({ length: 6 }).map((_, index) => {
  434. return `/9f/money${index + 1}.png`;
  435. });
  436. return (
  437. <Mask visible={visible} destroyOnClose={true} getContainer={element.current} opacity={0.75}>
  438. <Snowfall images={ImagesData} onClose={() => setVisible(false)} />
  439. {status === Status.is_start ? (
  440. <PacketStartDetail
  441. onCloseHby={() => setVisible(false)}
  442. onReciveRed={onReciveRed}
  443. iconImg={iconLists[1]}
  444. packet={packetCurrent.current!}
  445. />
  446. ) : status === Status.is_receive ? (
  447. <PacketReceiveDetail
  448. onCloseHby={() => setVisible(false)}
  449. redAmount={redAmount}
  450. iconImg={iconLists[2]}
  451. />
  452. ) : (
  453. <PacketEndDetail
  454. onCloseHby={() => setVisible(false)}
  455. iconImg={iconLists[0]}
  456. packet={packetCurrent.current!}
  457. />
  458. )}
  459. </Mask>
  460. );
  461. });
  462. export default memo(RedPacketModal);