index.tsx 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523
  1. "use client";
  2. import { getWheelRunApi, WheelsType } from "@/api/cashWheel";
  3. import { useRouter } from "@/i18n/routing";
  4. import useWheelStore from "@/stores/useWheelStore";
  5. import { percentage, timeFormat } from "@/utils/methods";
  6. import { LuckyWheel } from "@lucky-canvas/react";
  7. import NumberFlow from "@number-flow/react";
  8. import { Mask, ProgressBar } from "antd-mobile";
  9. import Image from "next/image";
  10. import { FC, forwardRef, memo, useEffect, useImperativeHandle, useRef, useState } from "react";
  11. import animation from "../animations.module.scss";
  12. import styles from "./style.module.scss";
  13. type Props = {
  14. onAfterHandler?: () => void;
  15. };
  16. export type WheelModalProps = {
  17. onClose: () => void;
  18. onOpen: (value: WheelsType) => void;
  19. };
  20. /**
  21. * 轮盘组件
  22. */
  23. const blocks = [
  24. {
  25. padding: "0",
  26. imgs: [
  27. {
  28. src: "/wheels/wheel.png",
  29. width: "100%",
  30. height: "100%",
  31. rotate: true,
  32. },
  33. ],
  34. },
  35. ];
  36. const prizes = [
  37. {
  38. fonts: [
  39. {
  40. text: "5000",
  41. top: "20%",
  42. fontColor: "#a47716",
  43. fontWeight: "bold",
  44. type: "bonus",
  45. fontSize: "0.1528rem",
  46. },
  47. ],
  48. },
  49. {
  50. fonts: [{ text: "", top: "20%", fontColor: "#a47716", fontWeight: "bold", type: "empty" }],
  51. imgs: [
  52. {
  53. src: "/wheels/prizes-empty.png",
  54. top: "20%",
  55. width: "0.2778rem",
  56. },
  57. ],
  58. },
  59. {
  60. fonts: [
  61. {
  62. text: "50",
  63. top: "20%",
  64. fontColor: "#a47716",
  65. fontWeight: "bold",
  66. type: "bonus",
  67. fontSize: "0.1528rem",
  68. },
  69. ],
  70. },
  71. {
  72. fonts: [
  73. { text: "", top: "30%", fontColor: "#a47716", fontWeight: "bold", type: "balance" },
  74. ],
  75. imgs: [
  76. {
  77. src: "/wheels/prizes-money.png",
  78. top: "20%",
  79. width: "0.3472rem",
  80. },
  81. ],
  82. },
  83. {
  84. fonts: [
  85. {
  86. text: "1000",
  87. top: "20%",
  88. fontColor: "#a47716",
  89. fontWeight: "bold",
  90. type: "bonus",
  91. fontSize: "0.1528rem",
  92. },
  93. ],
  94. },
  95. {
  96. imgs: [
  97. {
  98. src: "/wheels/prizes-empty.png",
  99. top: "20%",
  100. width: "0.2778rem",
  101. height: "0.2778rem",
  102. },
  103. ],
  104. },
  105. {
  106. fonts: [
  107. {
  108. text: "1",
  109. top: "20%",
  110. fontColor: "#a47716",
  111. fontWeight: "bold",
  112. type: "bonus",
  113. fontSize: "0.1528rem",
  114. },
  115. ],
  116. },
  117. {
  118. fonts: [
  119. {
  120. text: "",
  121. top: "20%",
  122. fontColor: "#a47716",
  123. fontWeight: "bold",
  124. type: "balance",
  125. fontSize: "0.1528rem",
  126. },
  127. ],
  128. imgs: [
  129. {
  130. src: "/wheels/prizes-money.png",
  131. top: "20%",
  132. width: "0.3472rem",
  133. },
  134. ],
  135. },
  136. ];
  137. const defaultConfig = {
  138. offsetDegree: 20,
  139. };
  140. /**
  141. * type: isRotate 是否可旋转, 分享页面不需要旋转, 而是跳转
  142. */
  143. export interface WheelProps {
  144. isRotate: boolean;
  145. onRotateEnd?: () => void;
  146. onRotateBefore?: () => void;
  147. onRotateDisable?: () => void;
  148. }
  149. const noop = () => {};
  150. export const WheelClient: FC<WheelProps> = (props) => {
  151. const { isRotate, onRotateEnd = noop, onRotateBefore = noop, onRotateDisable = noop } = props;
  152. const { statusWheel, currentWheel, setWheel } = useWheelStore((state) => ({
  153. statusWheel: state.status,
  154. currentWheel: state.currentWheel,
  155. setWheel: state.setWheel,
  156. }));
  157. // 选中 dom
  158. const activeRef = useRef<HTMLImageElement | null>(null);
  159. const wheelRef = useRef<any>();
  160. /*是否旋转*/
  161. const rotating = useRef<boolean>(false);
  162. const router = useRouter();
  163. const [isWin, setIsWin] = useState(false);
  164. const [buttonText, setButtonText] = useState<number>(0); // 0 -> false 1:true
  165. // 当前中奖
  166. const currentWin = useRef<any>({});
  167. const startRotate = (key: string) => {
  168. if (!isRotate) {
  169. router.push("/register");
  170. return;
  171. }
  172. // 正在旋转中
  173. if (rotating.current) return;
  174. // 如果是没有旋转次数
  175. if (currentWheel.can === 0) {
  176. onRotateDisable();
  177. }
  178. if (statusWheel !== 1) return;
  179. // 开始旋转中
  180. wheelRef.current?.play();
  181. // 点击抽奖按钮会触发star回调
  182. rotating.current = true;
  183. // 开始旋转前回调
  184. onRotateBefore();
  185. getWheelRunApi({ activity_id: currentWheel.id! })
  186. .then((res) => {
  187. setTimeout(() => {
  188. wheelRef.current?.stop(res.data.index);
  189. currentWin.current = res.data;
  190. }, 2000);
  191. })
  192. .catch(() => {
  193. wheelRef.current?.init();
  194. });
  195. };
  196. const endRotate = (prize: any) => {
  197. activeRef.current!.style.display = "block";
  198. if (currentWin.current.amount > 0) {
  199. setIsWin(true);
  200. setButtonText(currentWin.current.amount);
  201. } else {
  202. setButtonText(currentWheel.can || 0);
  203. setIsWin(false);
  204. }
  205. setTimeout(() => {
  206. setWheel().then((data) => {
  207. rotating.current = false;
  208. if (activeRef.current) {
  209. activeRef.current.style.display = "none";
  210. }
  211. onRotateEnd && onRotateEnd();
  212. setButtonText(data?.activate.can || 0);
  213. });
  214. // 重置状态
  215. setIsWin(false);
  216. }, 2000);
  217. };
  218. useEffect(() => {
  219. setButtonText(currentWheel.can || 0);
  220. }, [currentWheel.can]);
  221. return (
  222. <div className={"relative w-[100%] transform"}>
  223. <Image
  224. src="/wheels/aura.png"
  225. alt=""
  226. width={750}
  227. height={300}
  228. className={`absolute -z-[1] h-[100%] ${animation.floatAnimation}`}
  229. />
  230. <div className={"absolute h-[3.0833rem] w-[100%] overflow-hidden"}>
  231. <Image
  232. width={750}
  233. height={300}
  234. src="/wheels/bg-light.png"
  235. className={`absolute -top-[0.2778rem] left-0 -z-[1] ${animation.rotateAnimation}`}
  236. alt=""
  237. />
  238. </div>
  239. <Image
  240. width={750}
  241. height={300}
  242. alt=""
  243. src={"/wheels/wheel-bg.png"}
  244. className={"mx-auto h-[5.6rem] w-[100%] object-cover"}
  245. />
  246. <Image
  247. width={750}
  248. height={300}
  249. alt=""
  250. src={"/wheels/title.png"}
  251. className={"absolute left-[13%] top-[29%] z-10 w-[72%]"}
  252. />
  253. <Image
  254. width={750}
  255. height={300}
  256. alt=""
  257. src={"/wheels/bg-buttom.png"}
  258. className={
  259. "absolute bottom-[0.35rem] left-[54%] z-20 w-[1.7361rem] -translate-x-1/2"
  260. }
  261. />
  262. {/*定位到中心圆*/}
  263. <div className={"absolute bottom-[19.1%] z-10 h-[2.68rem] w-[100%]"}>
  264. <div className={"relative flex h-[100%] w-[100%] justify-center"}>
  265. <img
  266. src="/wheels/light-1.png"
  267. alt=""
  268. className={`absolute h-[100%] ${animation.flashingAnimation}`}
  269. />
  270. <img
  271. src="/wheels/light-2.png"
  272. alt=""
  273. className={`absolute h-[100%] ${animation.antiFlashingAnimation}`}
  274. />
  275. {/*<img*/}
  276. {/* src="/wheels/light-3.png"*/}
  277. {/* alt=""*/}
  278. {/* className={`absolute h-[100%] ${styles.closeFlashing}`}*/}
  279. {/*/>*/}
  280. <img
  281. ref={activeRef}
  282. src="/wheels/active-bg.png"
  283. className={`absolute z-10 ml-[0] mt-[0.159rem] hidden h-[1.3rem] ${animation.activeAnimation}`}
  284. alt=""
  285. />
  286. <div className={"relative h-[100%] w-[2.7rem] rounded-[50%] p-[0.14rem]"}>
  287. <LuckyWheel
  288. ref={wheelRef}
  289. width="2.42rem"
  290. height="2.42rem"
  291. blocks={blocks}
  292. defaultConfig={defaultConfig}
  293. prizes={prizes}
  294. onEnd={(prize: any) => endRotate(prize)}
  295. />
  296. <div
  297. className={
  298. "absolute bottom-[50%] left-0 z-[888] w-[100%] " +
  299. " translate-y-1/2"
  300. }
  301. >
  302. <div
  303. className={"relative flex h-[1.8rem] justify-center"}
  304. onClick={() => startRotate("desktop")}
  305. >
  306. <Image
  307. src={"/wheels/pointer.png"}
  308. className={"h-[1.8rem]" + " object-contain"}
  309. width={200}
  310. height={150}
  311. alt={"start"}
  312. />
  313. <div
  314. className={
  315. "absolute bottom-[50%] translate-y-1/2 text-center" +
  316. " text-[#ffdb0e]" +
  317. " text-[0.2222rem] font-black"
  318. }
  319. >
  320. <p className={"-mt-[0.2778rem] h-[0.22rem]"}>
  321. {buttonText > 0 && isWin ? "+" : ""}
  322. </p>
  323. <NumberFlow value={buttonText} />
  324. </div>
  325. </div>
  326. </div>
  327. </div>
  328. </div>
  329. </div>
  330. </div>
  331. );
  332. };
  333. export const LeftListClient = () => {
  334. const allHistory: any[] = Array.from({ length: 100 }).map((item) => ({
  335. phone_number: `55****${Math.floor(Math.random() * 10000)
  336. .toString()
  337. .padStart(4, "0")}`,
  338. receive_time: Date.now(),
  339. }));
  340. return (
  341. <>
  342. <div className={`${styles.winList} ${styles.swipernoswiping} ${styles.type2}`}>
  343. {allHistory &&
  344. allHistory.length > 0 &&
  345. allHistory?.map((item, index) => {
  346. return (
  347. <div className={styles.item} key={index}>
  348. <span className={`${styles.name} ${styles.omitWrap}`}>
  349. {item.phone_number}
  350. </span>
  351. <span className={styles.tipText}>
  352. {timeFormat(item.receive_time, "br", undefined, true)}
  353. </span>
  354. <div className={styles.value}>
  355. <span className={styles.addCash}>+100</span>
  356. <span className={styles.unit}> R$</span>
  357. </div>
  358. </div>
  359. );
  360. })}
  361. </div>
  362. </>
  363. );
  364. };
  365. const DetailClient = (props: { onUnload: () => void }) => {
  366. const { onUnload } = props;
  367. const router = useRouter();
  368. const { totalCount, currentWheel, setWheel } = useWheelStore((state) => ({
  369. statusWheel: state.status,
  370. currentWheel: state.currentWheel,
  371. setWheel: state.setWheel,
  372. totalCount: state.totalCount,
  373. }));
  374. const [count, setCount] = useState(0);
  375. const goPage = () => {
  376. router.push("/cashWheel");
  377. onUnload();
  378. };
  379. useEffect(() => {
  380. setCount(currentWheel.amount || 0);
  381. }, [currentWheel.amount]);
  382. return (
  383. <div className={`${styles.cashMain} ${styles.cashMain} ${styles.type1}`}>
  384. <div className={styles.haveCash}>
  385. <img src="/wheel/cash.png" alt="" className={styles.cashImg} />
  386. <div className={styles.cash}>
  387. <NumberFlow value={count} prefix={"R$ "} trend={+1} />
  388. </div>
  389. <span className={styles.withdraw}>
  390. <img src="/wheel/pix.png" alt="" /> SACAR{" "}
  391. </span>
  392. </div>
  393. <div className={styles.progress}>
  394. <div className={styles.num}> {percentage(count, 100)}%</div>
  395. {/*<div className={styles.bar}>*/}
  396. {/* <span style={{ width: "calc(97.15% - 1rem)" }}></span>*/}
  397. {/*</div>*/}
  398. <ProgressBar
  399. percent={percentage(count, 100)}
  400. style={{
  401. "--fill-color": "#fb8b05",
  402. "--track-width": "0.0694rem",
  403. }}
  404. />
  405. </div>
  406. <div className={styles.needCash}>
  407. Ainda e necessário{" "}
  408. <span className={styles.needCashNum}>
  409. {" "}
  410. <NumberFlow value={totalCount - count} trend={-1} />
  411. </span>{" "}
  412. para realizar do saque{" "}
  413. </div>
  414. <div
  415. className={
  416. "h-[0.34rem] w-[100%] rounded-[0.0694rem] bg-[#fb8b05] text-[#fff]" +
  417. " flex items-center justify-center"
  418. }
  419. onClick={goPage}
  420. >
  421. Reivindique mais para sacar
  422. </div>
  423. <div className={"mt-[10px] h-[2.0833rem] w-[100%] overflow-hidden"}>
  424. <LeftListClient />
  425. </div>
  426. </div>
  427. );
  428. };
  429. const WheelModal = forwardRef<WheelModalProps, Props>(function RedPacketModal(props, ref) {
  430. const [visible, setVisible] = useState(false);
  431. const [detailsVisible, setDetailsVisible] = useState(false);
  432. useImperativeHandle(ref, () => {
  433. return {
  434. onClose: () => setVisible(false),
  435. onOpen: () => {
  436. setVisible(true);
  437. },
  438. };
  439. });
  440. const onRotateEnd = () => {
  441. setVisible(false);
  442. setDetailsVisible(true);
  443. };
  444. const onUnload = () => {
  445. setDetailsVisible(false);
  446. };
  447. return (
  448. <>
  449. <Mask
  450. visible={detailsVisible}
  451. getContainer={null}
  452. destroyOnClose={true}
  453. opacity="thick"
  454. >
  455. <div className={"absolute top-[18%] z-50 w-[100%] px-[0.1389rem]"}>
  456. <div className={"rounded-[0.0694rem] bg-[#232327FF] p-[0.0694rem]"}>
  457. <div className={"flex items-center"}>
  458. <div className={"flex flex-1"}>
  459. <Image
  460. src={"/wheels/prizes-money.png"}
  461. width={40}
  462. height={40}
  463. alt={"moeny"}
  464. ></Image>
  465. <span className={"ml-[0.0694rem]"}>Receba R$ 100 de graca</span>
  466. </div>
  467. <span
  468. className={"iconfont icon-guanbi"}
  469. onClick={() => setDetailsVisible(false)}
  470. ></span>
  471. </div>
  472. <DetailClient onUnload={onUnload} />
  473. </div>
  474. </div>
  475. </Mask>
  476. <Mask visible={visible} destroyOnClose={true} getContainer={null} opacity="thick">
  477. <div
  478. className={"absolute right-[0.2083rem] top-[18%] z-50"}
  479. onClick={() => setVisible(false)}
  480. >
  481. <span className={"iconfont icon-guanbi"}></span>
  482. </div>
  483. <div className={"absolute top-[5%] w-[100%]"}>
  484. <WheelClient isRotate={true} onRotateEnd={onRotateEnd} />
  485. </div>
  486. </Mask>
  487. </>
  488. );
  489. });
  490. export default memo(WheelModal);