1
0

2 Коміти 575c3f8aa7 ... 3442e8b884

Автор SHA1 Опис Дата
  PPPPPPP 3442e8b884 no message 4 тижнів тому
  PPPPPPP ecac6f8e05 bugsfix 5 місяців тому
35 змінених файлів з 1288 додано та 255 видалено
  1. 17 11
      public/index.html
  2. 69 13
      public/js/load.m.js
  3. 6 2
      src/App.js
  4. 19 0
      src/Global.js
  5. 221 4
      src/components/AutoWinList/index.jsx
  6. 273 0
      src/components/AutoWinList/styles.module.scss
  7. 5 3
      src/features/mainSlice.js
  8. 52 0
      src/features/pageSlice.js
  9. 11 64
      src/hooks/usePaymentActions.js
  10. 90 0
      src/hooks/usePaymentGate.js
  11. 12 1
      src/layout/Navbar/index.jsx
  12. 6 1
      src/layout/Routes/RouteContainer.jsx
  13. 7 1
      src/locales/languages/en_US.json
  14. 10 2
      src/locales/languages/es_MX.json
  15. 10 2
      src/locales/languages/pt_BR.json
  16. 26 1
      src/styles/_elements.scss
  17. 5 0
      src/styles/_global.scss
  18. 63 0
      src/utils/PwaDetect.js
  19. 30 0
      src/utils/PwaHelp.js
  20. 21 1
      src/utils/UAUtils.js
  21. 37 8
      src/widgets/TopDownload/index.jsx
  22. 0 5
      src/widgets/TopDownload/styles.module.scss
  23. 2 1
      src/widgets/WBonus/styles.module.scss
  24. 8 47
      src/widgets/WBonusPack/index.jsx
  25. 8 46
      src/widgets/WDeposit/BR/BRDeposit.jsx
  26. 1 1
      src/widgets/WEditProfile/index.jsx
  27. 65 3
      src/widgets/WGame/WDGame/WDGame.jsx
  28. 61 2
      src/widgets/WGame/WDGame/styles.module.scss
  29. 22 2
      src/widgets/WInstallBox/WQuickInstall/index.jsx
  30. 28 1
      src/widgets/WInstallBox/WSecurityScan/index.jsx
  31. 28 1
      src/widgets/WInstallBox/WSecurityScan/styles.module.scss
  32. 51 23
      src/widgets/WInstallBox/WShortcutGuide/index.jsx
  33. 11 3
      src/widgets/WPaymentOrder/MX/MXPayOrder.jsx
  34. 9 3
      src/widgets/WPromotion2/styles.module.scss
  35. 4 3
      src/widgets/WWithdraw/BR/BRWithdraw.jsx

+ 17 - 11
public/index.html

@@ -12,7 +12,13 @@
 
     <meta name="description" content="%%BRAND_DESCRIPTION%%" />
 
-    <meta name="theme-color" content="%%THEME_COLOR%%" />
+    <!-- 沉浸式:状态栏 + Android 导航栏跟着 theme-color 走,必须和 --body-dark
+         (#1b1c20) 完全一致,否则会和 app 内容差出色阶。还需 color-scheme:dark
+         告诉系统这是深色界面,gesture bar / 文字默认走亮色不会变白 -->
+    <meta name="color-scheme" content="dark" />
+    <meta name="theme-color" content="#1b1c20" media="(prefers-color-scheme: dark)" />
+    <meta name="theme-color" content="#1b1c20" media="(prefers-color-scheme: light)" />
+    <meta name="theme-color" content="#1b1c20" />
 
     <meta property="og:type" content="website">
     <meta property="og:site_name" content="%%BRAND_NAME%%">
@@ -43,20 +49,20 @@
     <div id="root"></div>
     <script type="text/javascript">
       function setThemeColor(){
-        var meta = document.querySelector('meta[name=theme-color]');
-        if (meta) {
-          var color="#1b1c20";
+        // 统一改:把同名所有 meta 都同步成当前主题色,沉浸式才不会有错位
+        var metas = document.querySelectorAll('meta[name=theme-color]');
+        if (!metas || metas.length===0) return;
+        var color="#1b1c20";
+        try {
           var preferences=localStorage.getItem('preferences');
-          if (preferences!=null){
+          if (preferences){
             var json=JSON.parse(preferences);
-            if (json&&json.theme==="light"){
-              color="#FAFAF8";
-            }
+            if (json && json.theme==="light") color="#FAFAF8";
           }
-          meta.setAttribute('content', color);
-        }
+        } catch(e){}
+        metas.forEach(function(m){ m.setAttribute('content', color); });
       }
-     
+
     </script>
   </body>
 </html>

+ 69 - 13
public/js/load.m.js

@@ -716,6 +716,8 @@
     // BRG77 - 巴西默认主站(金币金 + 巴西绿 + 火焰红)
     "br24680": {
       "title": "24680.com Cassino tradicional com dinheiro real",
+      // 安装到桌面/启动器后显示的简短名(PWA short_name),≤12 字符最稳
+      "short_name": "24680.com",
       "subtitle": "Sequência Par: Dobre Sua Sorte no 24680.com! Melhor Cassino. Maior jogo crash. Comprovadamente justo & Dealer ao vivo. 10000 jogos de slot. Bônus únicos & rodadas grátis da sorte.",
       "product": "24680",
       "product_id": "37",
@@ -762,8 +764,8 @@
       start_url: start_url,
       scope: './',
       display: 'standalone',
-      background_color: '#2e303f',
-      theme_color: '#2e303f',
+      background_color: '#1b1c20',
+      theme_color: '#1b1c20',
       icons: [
         {
           src: `${beginurl}${icon_path}`,
@@ -807,10 +809,18 @@
       ],
       protocol_handlers:[
         {
-          protocol:'web+usslot',
+          // 自定义协议:W3C 要求 web+ 开头 + 纯小写字母。这里用 web+brslot
+          // (Brazil + slot),openDesktopApp() 会用 web+brslot:// 拉起 PWA。
+          protocol:'web+brslot',
           url:`${siteurl}?launchedfrom=homescreen&pwaprotocolredirect=%s`
         }
-      ]
+      ],
+      // 自指 related_applications:让 navigator.getInstalledRelatedApps() 在
+      // Android Chrome 上能返回这个 PWA → 我们就能轮询探测"装好了没"
+      related_applications:[
+        { platform: 'webapp', url: `${beginurl}manifest.json` }
+      ],
+      prefer_related_applications: false
     };
   };
   const getSettings = async () => {
@@ -825,12 +835,29 @@
     // }
     // const content = await response.json();
     const defaultContent = geyCommonJSON();
+    // PWA 名字优先级:
+    //   short_name 用站点短名(桌面图标显示用)
+    //   name 也用同一个短名 —— Android 在系统设置/最近任务/分享菜单都会读 name,
+    //     如果用 title("24680.com Cassino tradicional com dinheiro real")会被截断
+    //     甚至换行,体验差。SEO 用 description 字段就行,不必在 manifest.name 里塞。
+    //   要保留长名做 SEO 时,在 site_config 里单独设置 manifest_name 字段即可。
+    const t = window.themeConfig || {};
+    const shortName = t.short_name
+      || t.product
+      || (t.title ? String(t.title).split(/[\s\-|·.]/).filter(Boolean)[0] : '')
+      || 'App';
+    const manifestName = t.manifest_name || shortName;
     const settings = Object.assign(defaultContent, {
-      name: window.themeConfig.title,
-      short_name: window.themeConfig.title,
-      description: window.themeConfig.subtitle,
-      lang: window.themeConfig.lang || 'en'
+      name: manifestName,
+      short_name: shortName,
+      description: t.subtitle,
+      lang: t.lang || 'en'
     });
+    // 把 manifest 用的协议暴露给 React 端,安装完后 openDesktopApp() 用它拉起 PWA
+    try {
+      const ph = (settings.protocol_handlers||[])[0];
+      if (ph && ph.protocol) window.PWA_PROTOCOL = ph.protocol;
+    } catch(e) {}
     return settings;
   };
   const getSitePrefix=()=>{
@@ -867,10 +894,26 @@
     const region = window.themeConfig?.regionConfig;
     // 设置 html lang 属性(优先使用完整 locale,如 pt-BR)
     document.documentElement.lang = region?.locale || lang;
-    // 设置页面标题(使用区域配置的标题后缀)
-    if (window.themeConfig?.title) {
+    // 设置页面标题。
+    //   关键:Android Chrome 在 manifest.name 抓不到时会用 document.title 作为
+    //   PWA 安装名,所以这里得让 title 以短名(short_name / product)开头,
+    //   把 SEO 用的长描述放在分隔符之后。这样即使 fallback 也只显示短名前缀。
+    const t = window.themeConfig || {};
+    const shortName = t.short_name || t.product || (t.title ? String(t.title).split(/[\s\-|·.]/).filter(Boolean)[0] : '');
+    if (t.title) {
       const titleSuffix = region?.titleSuffix || 'Online Slots & Real Money Games';
-      document.title = `${window.themeConfig.title} | ${titleSuffix}`;
+      const head = shortName || t.title;
+      document.title = `${head} | ${titleSuffix}`;
+    }
+    // iOS Safari 添加到主屏幕用这个名字(不是 manifest.name)
+    if (shortName) {
+      const setMeta = (name, content) => {
+        let m = document.querySelector(`meta[name="${name}"]`);
+        if (!m) { m = document.createElement('meta'); m.name = name; document.head.appendChild(m); }
+        m.content = content;
+      };
+      setMeta('apple-mobile-web-app-title', shortName);
+      setMeta('application-name', shortName);
     }
     // 设置 favicon
     if (window.themeConfig?.favicon) {
@@ -921,14 +964,27 @@
     initSiteConfig();
     const settings = await getSettings();
     const str = JSON.stringify(settings, null, 2);
-    // console.log(settings, str);
     let i = document.querySelector('link[rel="manifest"]');
     if (!i) {
       i = document.createElement('link');
       i.rel = 'manifest';
       document.head.appendChild(i);
     }
-    i.href = 'data:text/json;charset=utf-8,' + encodeURIComponent(str);
+    // Blob URL 比 data: URL 在 Android Chrome 安装 PWA 时更可靠:
+    // data: URL 在某些版本下 Chrome 抓不到 manifest.name,会回退到
+    // document.title 当作 app 名(导致安装出来的图标名很长)。
+    // Blob URL 走标准 fetch 路径,正确 mime + utf-8,name 取得稳。
+    try {
+      const blob = new Blob([str], { type: 'application/manifest+json' });
+      // 旧的 blob URL 释放掉,避免内存泄漏
+      if (i.href && i.href.indexOf('blob:') === 0) {
+        try { URL.revokeObjectURL(i.href); } catch(e){}
+      }
+      i.href = URL.createObjectURL(blob);
+    } catch(e) {
+      // 兜底:浏览器不支持 Blob 时仍用 data: URL
+      i.href = 'data:application/manifest+json;charset=utf-8,' + encodeURIComponent(str);
+    }
   };
 
   load();

+ 6 - 2
src/App.js

@@ -225,8 +225,12 @@ const App = () => {
         {
             dispatch(disconnectHall());
         }
-        //pay
-        // reqPayConfig(dispatch,userInfo);
+        //pay:登录后即拉 recharge config,让 cpfFirst 提前到位
+        // 后续所有支付入口(首充 / 充值 / BonusPack / Promotion 等)
+        // 都依赖 paySlice 的 cpfFirst 做 BR-CPF 拦截
+        if (userID>0){
+            reqPayConfig(dispatch,userInfo);
+        }
         // dispatch(reqFirstPayConfig(userID))
         //customer
         // dispatch(reqCustomService());

+ 19 - 0
src/Global.js

@@ -263,5 +263,24 @@ global.showQuickInstall=()=>{
 global.hideQuickInstall=()=>{
     if (global.quickInstall) global.quickInstall.hide();
 }
+// PWA 安装弹窗计数 + 订阅:当 SecurityScan / QuickInstall / ShortcutGuide 打开时,
+// TopDownload 横幅需要让出空间避免视觉冲突。开任一弹窗 +1,关闭 -1,订阅者收到通知。
+global._pwaModalCount = 0;
+global._pwaModalSubs = new Set();
+global.notifyPwaModal = () => {
+    global._pwaModalSubs.forEach(fn => { try { fn(global._pwaModalCount); } catch(e){} });
+};
+global.pushPwaModal = () => {
+    global._pwaModalCount++;
+    global.notifyPwaModal();
+};
+global.popPwaModal = () => {
+    global._pwaModalCount = Math.max(0, global._pwaModalCount - 1);
+    global.notifyPwaModal();
+};
+global.subscribePwaModal = (fn) => {
+    global._pwaModalSubs.add(fn);
+    return () => global._pwaModalSubs.delete(fn);
+};
 
 //wd pass

+ 221 - 4
src/components/AutoWinList/index.jsx

@@ -17,8 +17,91 @@ import {useNavigate} from "react-router";
 import {numFormatter} from "@utils/helpers";
 import LazyImgix from "@components/Imgix/LazyImgix";
 import {useSelector} from "react-redux";
-import {getOrderedGames} from "@features/pageSlice";
+import {getOrderedGames, getHotGames} from "@features/pageSlice";
 import {getConfig} from "@features/paySlice";
+import {getAvatarUrl} from "@utils/avatar";
+import {getVirtualUsers} from "@constants/virtualUser";
+
+// win 金额兜底池:后端返回的 src.win 经常出现 0 / 极小值,看起来很假,
+// 没有有效金额时按这个权重池随机一个 R$ 数值,落在常见小奖区间。
+const WIN_AMOUNT_TIERS  = [5, 10, 20, 50, 100, 200, 500, 1000, 2000, 5000];
+const WIN_AMOUNT_WEIGHTS= [10,18, 20, 18, 12,  10,  6,    3,    2,    1];
+const pickWinAmount = () => pickWeighted(WIN_AMOUNT_TIERS, WIN_AMOUNT_WEIGHTS);
+// 给 win cell 一个像样的金额(< 1 视为无效,重新抽)
+const sanitizeWin = (raw) => {
+    const n = Number(raw);
+    if (!Number.isFinite(n) || n < 1) return pickWinAmount();
+    return n;
+};
+
+// 头像池 1..15,跟个人资料 / Navbar 用同一组(avatar_*.png)。
+// CDN 上目前只有 15 张(avatar_16 会 404),保持和 new2 一致。
+// 用 nick 做哈希取模,让同一个昵称在轮播里头像稳定,不抖动。
+const AVATAR_POOL_SIZE = 15;
+const avatarIdFromNick = (nick) => {
+    const s = String(nick || "");
+    let h = 0;
+    for (let i = 0; i < s.length; i++) {
+        h = (h * 31 + s.charCodeAt(i)) | 0;
+    }
+    return Math.abs(h) % AVATAR_POOL_SIZE + 1;
+};
+
+// 把后端返回的 wins 列表展平成 desktop 单行轮播单元,
+// 65% win / 20% deposit / 15% withdraw。
+// !!! 注意 !!!
+//   1. 后端 wins 数据里的 item.img 是圆头像,不是游戏卡片图,所以游戏图必须
+//      从 hotGames / orderedGames 里抽(80% 热门 / 20% 全量)。
+//   2. 后端 src.nick / src.win 经常出现重复或 0 值,看起来很假;这里
+//      昵称统一用 getVirtualUsers 重新生成(90% UXXXX / 10% 脱敏名),
+//      win 金额遇到 0/无效 用 pickWinAmount 兜底。
+const buildFlatCells = (list, hotGames, orderedGames, depositPicker, count = 30) => {
+    const cells = [];
+    if (!list || list.length === 0) return cells;
+    const pickDeposit = typeof depositPicker === "function" ? depositPicker : pickDepositAmount;
+    const hot = Array.isArray(hotGames) ? hotGames : [];
+    const all = Array.isArray(orderedGames) ? orderedGames : [];
+    const pickGame = () => {
+        const useHot = Math.random() < 0.8 && hot.length > 0;
+        const pool = useHot ? hot : (all.length > 0 ? all : hot);
+        if (pool.length === 0) return null;
+        return pickWeightedGame(pool);
+    };
+    // 一次性生成一组虚拟昵称,每个 cell 拿一个,不重复 src.nick。
+    const nicks = getVirtualUsers(count);
+    for (let i = 0; i < count; i++) {
+        const src = list[i % list.length];
+        const nick = nicks[i] || `U${Math.floor(1000 + Math.random() * 9000)}`;
+        const r = Math.random();
+        let cell;
+        if (r < 0.65) {
+            const game = pickGame();
+            cell = {
+                kind: "win",
+                img: game ? game.img : src.img,
+                nick,
+                // src.win 0 或缺失时随机一个,避免出现 R$0.00 这种穿帮金额
+                amount: sanitizeWin(src.win),
+                link: game && game.id != null ? `/game/${game.id}` : src.link,
+            };
+        } else if (r < 0.85) {
+            cell = {
+                kind: "deposit",
+                nick,
+                amount: pickDeposit(),
+            };
+        } else {
+            cell = {
+                kind: "withdraw",
+                nick,
+                amount: pickWithdrawAmount(Number(src.win) || 0),
+            };
+        }
+        cell.avatarId = avatarIdFromNick(cell.nick);
+        cells.push(cell);
+    }
+    return cells;
+};
 
 
 const i18n=global.i18n;
@@ -220,6 +303,110 @@ const VRotator=({queue,formatGolds,onClickGame})=>{
     }
     return <Entry item={item} idx={idx} phase={phase} formatGolds={formatGolds} onClickGame={onClickGame} wrapClass={wrapClass}/>;
 };
+// 桌面版双行 Live Feed —— 飞机播报板风格:
+//  · 行内每张卡片 = [游戏图标 / 充值&提现图示] + [统一头像] + [Nick + 标签] + [金额]
+//  · 双行反向滚动 + hover 整体暂停 + 入场 flap-flip 动画
+//  · 仅 win 类型可点击跳游戏;充值/提现卡片不可点击(按需求)
+const FeedCell = ({cell, formatGolds, onClickGame}) => {
+    const isWin = cell.kind === "win";
+    const tagKey = isWin ? "win_list.tag_win" : (cell.kind === "deposit" ? "win_list.tag_deposit" : "win_list.tag_withdraw");
+    const onClick = isWin && cell.link ? () => onClickGame(cell.link) : undefined;
+    const cellClass = `${styles.feedCell} ${styles[`feedCell_${cell.kind}`]} ${isWin ? styles.feedCell_clickable : ""}`;
+    return (
+        <div className={cellClass} onClick={onClick} title={isWin ? "" : undefined}>
+            <div className={styles.feedIcon}>
+                {isWin ? (
+                    <LazyImgix src={cell.img} alt={"game-icon"} width={64} className={styles.feedGameImg}/>
+                ) : (
+                    <div className={`${styles.feedTxnIcon} ${styles[`feedTxn_${cell.kind}`]}`}>
+                        {cell.kind === "deposit" ? "+" : "↑"}
+                    </div>
+                )}
+            </div>
+            <img className={styles.feedAvatar} src={getAvatarUrl(cell.avatarId)} alt={"avatar"}/>
+            <div className={styles.feedMid}>
+                <div className={styles.feedNick}>{cell.nick}</div>
+                <div className={`${styles.feedTag} ${styles[`feedTag_${cell.kind}`]}`}>{i18n.t(tagKey)}</div>
+            </div>
+            <div className={`${styles.feedAmount} ${styles[`feedAmount_${cell.kind}`]}`}>
+                {formatGolds(cell.amount)}
+            </div>
+        </div>
+    );
+};
+
+// 桌面 LiveFeed —— 不再用持续滚动,改成"批次秀":
+// 一批 PER_BATCH 张卡片整批入场(带 stagger)→ 停顿 → 整批出场 → 切下一批,
+// 每批从 SHOW_MODES 里随机挑一种动画(避免连续重复同一种)。
+const SHOW_MODES = ["flyRight", "dropTop", "fanCenter", "shuffleAlt", "wipeLR", "popBounce"];
+const PER_BATCH = 5;
+
+const pickNextMode = (prev) => {
+    const others = SHOW_MODES.filter(m => m !== prev);
+    return others[Math.floor(Math.random() * others.length)];
+};
+
+const LiveFeedShow = ({cells, formatGolds, onClickGame}) => {
+    const total = cells ? cells.length : 0;
+    const [batchIdx, setBatchIdx] = React.useState(0);
+    const [mode, setMode] = React.useState(() => SHOW_MODES[Math.floor(Math.random() * SHOW_MODES.length)]);
+    const [phase, setPhase] = React.useState("in"); // in -> hold -> out
+
+    const batch = React.useMemo(() => {
+        if (total === 0) return [];
+        const start = (batchIdx * PER_BATCH) % total;
+        const arr = [];
+        for (let i = 0; i < PER_BATCH; i++) arr.push(cells[(start + i) % total]);
+        return arr;
+    }, [cells, batchIdx, total]);
+
+    React.useEffect(() => {
+        if (total === 0) return;
+        let t;
+        const inDur = 700 + (PER_BATCH - 1) * 180;   // 整批入场总时长
+        const holdDur = 2600;                         // 停顿
+        const outDur = 600 + (PER_BATCH - 1) * 120;   // 整批出场总时长
+        if (phase === "in") {
+            t = setTimeout(() => setPhase("hold"), inDur);
+        } else if (phase === "hold") {
+            t = setTimeout(() => setPhase("out"), holdDur);
+        } else {
+            t = setTimeout(() => {
+                setMode(prev => pickNextMode(prev));
+                setBatchIdx(i => i + 1);
+                setPhase("in");
+            }, outDur);
+        }
+        return () => { if (t) clearTimeout(t); };
+    }, [phase, total]);
+
+    if (total === 0) return null;
+
+    return (
+        <div className={styles.showStage}>
+            {batch.map((cell, i) => {
+                const animKey = phase === "out" ? `out_${mode}` : phase === "in" ? `in_${mode}` : null;
+                const animClass = animKey ? styles[animKey] : "";
+                // 入场正向 stagger,出场反向 stagger(最后一张先飞走,看着更自然)
+                const delaySec = phase === "in"
+                    ? i * 0.18
+                    : phase === "out"
+                        ? (PER_BATCH - 1 - i) * 0.12
+                        : 0;
+                return (
+                    <div
+                        key={`${batchIdx}_${i}`}
+                        className={`${styles.showCell} ${animClass}`}
+                        style={{animationDelay: `${delaySec}s`}}
+                    >
+                        <FeedCell cell={cell} formatGolds={formatGolds} onClickGame={onClickGame}/>
+                    </div>
+                );
+            })}
+        </div>
+    );
+};
+
 const AutoWinList=({data})=>{
     const {isMobile} = useThemeProvider();
     const [currIndex,setCurrIndex]=useState(0);
@@ -228,6 +415,7 @@ const AutoWinList=({data})=>{
     const navigate=useNavigate();
     const lvicon=[lv_1,lv_2,lv_3];
     const orderedGames=useSelector(getOrderedGames);
+    const hotGames=useSelector(getHotGames);
     const rechargeConfig=useSelector(getConfig);
     if (data==null)data={};
     // data={
@@ -492,11 +680,40 @@ const AutoWinList=({data})=>{
             navigate(link);
         }
     }
+    // 桌面 LiveFeed 扁平单元池:一次性生成 100 条数据缓存到 ref,
+    // 之后所有刷新(包括语言切换、redux 引用变化)都直接复用同一池子,
+    // i18n.t 只在 render 时换标签文案,nick/avatar/amount/img/kind 不抖。
+    // 缓存按 tab(currIndex) 维度存放;切 Tab 后再切回也保持一致。
+    const flatCellsCacheRef = React.useRef({});
+    const [flatCells, setFlatCells] = React.useState([]);
+    React.useEffect(() => {
+        const cache = flatCellsCacheRef.current;
+        const cached = cache[currIndex];
+        if (cached && cached.length > 0) {
+            setFlatCells(cached);
+            return;
+        }
+        const filtered = getAutoList();
+        if (!filtered || filtered.length === 0) return;
+        const hasGames = (hotGames && hotGames.length > 0) || (orderedGames && orderedGames.length > 0);
+        if (!hasGames) return;
+        const pool = buildFlatCells(filtered, hotGames, orderedGames, depositPicker, 100);
+        cache[currIndex] = pool;
+        setFlatCells(pool);
+        // eslint-disable-next-line react-hooks/exhaustive-deps
+    }, [data, currIndex, hotGames, orderedGames, depositPicker]);
+    const renderLiveFeedDesktop = () => (
+        <LiveFeedShow
+            cells={flatCells}
+            formatGolds={formatGolds}
+            onClickGame={(link)=>onClickLink(link)}
+        />
+    );
     const renderDesktop=()=>{
         return (
             <div className={styles.list_context}>
-                {renderTabs()}
-                {renderHAuto()}
+                {/* 单行 LiveFeed,去掉 All / Slots 标签,UI 更干净 */}
+                {renderLiveFeedDesktop()}
             </div>
         )
     }
@@ -513,7 +730,7 @@ const AutoWinList=({data})=>{
     }
     return (
         <div className={`w-100 ${isMobile()?styles.win_list_container_m:styles.win_list_container}`}>
-            {!isMobile() && renderTitle()}
+            {/* "Recent Big Wins" 标题去掉,桌面只剩一条 LiveFeed 跑马灯 */}
             {renderContext()}
         </div>
     )

+ 273 - 0
src/components/AutoWinList/styles.module.scss

@@ -373,3 +373,276 @@
     }
   }
 }
+
+/* ────────────────────────────────────────────────────────────
+ * Desktop Live Feed —— 飞机播报板风格双行轮播
+ * 单元卡片 = 游戏图标 / 充值&提现图示 + 头像 + Nick + 标签 + 金额
+ * ──────────────────────────────────────────────────────────── */
+.feedRows {
+  display: flex;
+  flex-direction: column;
+  gap: 8px;
+  width: 100%;
+  padding: 4px 0 2px;
+  user-select: none;
+}
+.feedRowMask {
+  width: 100%;
+  overflow: hidden;
+  position: relative;
+  /* 左右淡出,避免硬切 */
+  -webkit-mask-image: linear-gradient(to right, transparent 0, #000 36px, #000 calc(100% - 36px), transparent 100%);
+          mask-image: linear-gradient(to right, transparent 0, #000 36px, #000 calc(100% - 36px), transparent 100%);
+}
+.feedRowTrack {
+  display: flex;
+  gap: 10px;
+  width: max-content;
+  will-change: transform;
+  animation: feedScrollLeftKf 55s linear infinite;
+  &:hover { animation-play-state: paused; }
+}
+.feedScrollLeft  { animation-name: feedScrollLeftKf; }
+.feedScrollRight { animation-name: feedScrollRightKf; }
+@keyframes feedScrollLeftKf {
+  0%   { transform: translateX(0); }
+  100% { transform: translateX(-50%); }
+}
+@keyframes feedScrollRightKf {
+  0%   { transform: translateX(-50%); }
+  100% { transform: translateX(0); }
+}
+
+.feedCell {
+  flex: 0 0 auto;
+  display: flex;
+  align-items: center;
+  gap: 10px;
+  width: 280px;
+  height: 64px;
+  padding: 8px 12px 8px 8px;
+  border-radius: 12px;
+  background: linear-gradient(135deg, rgba(255,255,255,0.04), rgba(255,255,255,0.02));
+  border: 1px solid rgba(255,255,255,0.06);
+  box-shadow: inset 0 1px 0 rgba(255,255,255,0.04), 0 1px 0 rgba(0,0,0,0.25);
+  transform-origin: center;
+  /* 入场 flap,给整行第一次出现时一点节奏感 */
+  animation: feedCellFlapKf 0.6s ease-out both;
+}
+.feedCell_clickable {
+  cursor: pointer;
+  transition: transform 0.18s ease, border-color 0.18s ease, box-shadow 0.18s ease;
+  &:hover {
+    transform: translateY(-2px) scale(1.02);
+    border-color: rgba(11, 230, 173, 0.45);
+    box-shadow: 0 4px 14px rgba(11, 230, 173, 0.18);
+  }
+}
+@keyframes feedCellFlapKf {
+  0%   { opacity: 0; transform: perspective(400px) rotateX(-50deg); }
+  60%  { opacity: 1; transform: perspective(400px) rotateX(8deg); }
+  100% { opacity: 1; transform: perspective(400px) rotateX(0); }
+}
+
+.feedIcon {
+  width: 48px;
+  height: 48px;
+  flex: 0 0 48px;
+  border-radius: 10px;
+  overflow: hidden;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  background: rgba(0,0,0,0.25);
+}
+.feedGameImg {
+  width: 100%;
+  height: 100%;
+  object-fit: cover;
+}
+.feedTxnIcon {
+  width: 100%;
+  height: 100%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-weight: 800;
+  font-size: 22px;
+  letter-spacing: 0;
+}
+.feedTxn_deposit  { color: #FFD700; background: rgba(255, 215, 0, 0.10); }
+.feedTxn_withdraw { color: #A78BFA; background: rgba(167, 139, 250, 0.12); }
+
+.feedAvatar {
+  width: 36px;
+  height: 36px;
+  flex: 0 0 36px;
+  border-radius: 50%;
+  border: 2px solid rgba(255,255,255,0.12);
+  background: #222;
+  object-fit: cover;
+}
+
+.feedMid {
+  flex: 1 1 auto;
+  min-width: 0;
+  display: flex;
+  flex-direction: column;
+  gap: 2px;
+  overflow: hidden;
+}
+.feedNick {
+  font-size: 13px;
+  font-weight: 600;
+  color: var(--header, #fff);
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  line-height: 1.1;
+}
+.feedTag {
+  font-size: 10px;
+  font-weight: 700;
+  letter-spacing: 0.04em;
+  text-transform: uppercase;
+  line-height: 1;
+  width: max-content;
+  padding: 2px 6px;
+  border-radius: 4px;
+}
+.feedTag_win      { color: #0BE6AD; background: rgba(11, 230, 173, 0.12); }
+.feedTag_deposit  { color: #FFD700; background: rgba(255, 215, 0, 0.12); }
+.feedTag_withdraw { color: #A78BFA; background: rgba(167, 139, 250, 0.14); }
+
+.feedAmount {
+  font-size: 14px;
+  font-weight: 800;
+  white-space: nowrap;
+  letter-spacing: 0;
+  font-variant-numeric: tabular-nums;
+  /* 金额做一个轻微跳动 + 阴影,模拟翻牌停在数字上的一刻 */
+  text-shadow: 0 1px 0 rgba(0,0,0,0.45);
+}
+.feedAmount_win      { color: #0BE6AD; }
+.feedAmount_deposit  { color: #FFD700; }
+.feedAmount_withdraw { color: #C4B5FD; }
+
+/* 移动端不用 LiveFeed,但容器存在时不要把全屏宽度撑出去 */
+@media (max-width: 760px) {
+  .feedRows { display: none; }
+  .showStage { display: none; }
+}
+
+/* ────────────────────────────────────────────────────────────
+ * Desktop Live Feed —— 批次秀(替代持续滚动)
+ * 一批 5 张卡片整批入场 → 停顿 → 整批出场,下一批随机换动画
+ * ──────────────────────────────────────────────────────────── */
+.showStage {
+  display: flex;
+  gap: 10px;
+  width: 100%;
+  padding: 4px 0 2px;
+  overflow: hidden;
+  perspective: 1200px;
+  min-height: 72px;
+  user-select: none;
+}
+.showCell {
+  flex: 1 1 0;
+  min-width: 0;
+  transform-origin: center;
+  /* 让内部 .feedCell 适配父容器宽度,不再硬撑 280px */
+  > .feedCell {
+    width: 100%;
+    /* 关掉旧的整卡 flap 入场,统一交给批次动画控制 */
+    animation: none;
+  }
+}
+
+/* ─── Mode 1: flyRight 右飞入 / 翻转消失 ─── */
+@keyframes inFlyRightKf {
+  0%   { opacity: 0; transform: translateX(120%) scale(.92); }
+  100% { opacity: 1; transform: translateX(0) scale(1); }
+}
+@keyframes outFlipXKf {
+  0%   { opacity: 1; transform: rotateX(0); }
+  60%  { opacity: 1; transform: rotateX(-60deg); }
+  100% { opacity: 0; transform: rotateX(-90deg); }
+}
+.in_flyRight  { animation: inFlyRightKf .6s cubic-bezier(.2,.8,.2,1) both; }
+.out_flyRight { animation: outFlipXKf .55s ease-in both; }
+
+/* ─── Mode 2: dropTop 上方坠落 / 向左滑出 ─── */
+@keyframes inDropTopKf {
+  0%   { opacity: 0; transform: translateY(-90px); }
+  60%  { opacity: 1; transform: translateY(8px); }
+  80%  { transform: translateY(-3px); }
+  100% { opacity: 1; transform: translateY(0); }
+}
+@keyframes outSlideLeftKf {
+  0%   { opacity: 1; transform: translateX(0); }
+  100% { opacity: 0; transform: translateX(-130%); }
+}
+.in_dropTop  { animation: inDropTopKf .65s cubic-bezier(.4,1.6,.5,1) both; }
+.out_dropTop { animation: outSlideLeftKf .55s cubic-bezier(.6,0,.8,.2) both; }
+
+/* ─── Mode 3: fanCenter 中心放大旋入 / 缩小旋出 ─── */
+@keyframes inFanCenterKf {
+  0%   { opacity: 0; transform: scale(.4) rotate(-14deg); }
+  100% { opacity: 1; transform: scale(1) rotate(0); }
+}
+@keyframes outFanCenterKf {
+  0%   { opacity: 1; transform: scale(1) rotate(0); }
+  100% { opacity: 0; transform: scale(.3) rotate(18deg); }
+}
+.in_fanCenter  { animation: inFanCenterKf .55s cubic-bezier(.2,.8,.2,1) both; }
+.out_fanCenter { animation: outFanCenterKf .5s ease-in both; }
+
+/* ─── Mode 4: shuffleAlt 左右交替推入 / 撕角飘落 ─── */
+@keyframes inFromLeftSkewKf {
+  0%   { opacity: 0; transform: translateX(-130%) skewX(8deg); }
+  100% { opacity: 1; transform: translateX(0) skewX(0); }
+}
+@keyframes inFromRightSkewKf {
+  0%   { opacity: 0; transform: translateX(130%) skewX(-8deg); }
+  100% { opacity: 1; transform: translateX(0) skewX(0); }
+}
+@keyframes outPeelKf {
+  0%   { opacity: 1; transform: rotate(0) translateY(0); }
+  100% { opacity: 0; transform: rotate(-14deg) translateY(28px); }
+}
+.in_shuffleAlt {
+  animation: inFromLeftSkewKf .55s cubic-bezier(.2,.8,.2,1) both;
+}
+.in_shuffleAlt:nth-child(even) {
+  animation-name: inFromRightSkewKf;
+}
+.out_shuffleAlt {
+  animation: outPeelKf .5s ease-in both;
+  transform-origin: bottom left;
+}
+
+/* ─── Mode 5: wipeLR 左到右擦显 / 右到左擦除 ─── */
+@keyframes inWipeLRKf {
+  0%   { opacity: .85; clip-path: inset(0 100% 0 0); }
+  100% { opacity: 1;   clip-path: inset(0 0 0 0); }
+}
+@keyframes outWipeRLKf {
+  0%   { opacity: 1; clip-path: inset(0 0 0 0); }
+  100% { opacity: 0; clip-path: inset(0 0 0 100%); }
+}
+.in_wipeLR  { animation: inWipeLRKf .55s ease-out both; }
+.out_wipeLR { animation: outWipeRLKf .5s ease-in both; }
+
+/* ─── Mode 6: popBounce 弹跳放大 / 爆裂收缩消失 ─── */
+@keyframes inPopBounceKf {
+  0%   { opacity: 0; transform: scale(.3) translateY(20px); }
+  60%  { opacity: 1; transform: scale(1.12) translateY(-4px); }
+  100% { opacity: 1; transform: scale(1) translateY(0); }
+}
+@keyframes outExplodeKf {
+  0%   { opacity: 1; transform: scale(1) rotate(0); }
+  100% { opacity: 0; transform: scale(.4) rotate(-15deg) translateY(20px); }
+}
+.in_popBounce  { animation: inPopBounceKf .6s cubic-bezier(.3,1.6,.5,1) both; }
+.out_popBounce { animation: outExplodeKf .5s ease-in both; }

+ 5 - 3
src/features/mainSlice.js

@@ -695,9 +695,11 @@ export const Main=createSlice({
         serviceLink:"",
         holidaysGiftInfo: {},
         holidaysActInfo: {},
-        // PWA 是否已安装(0 未安装、1 已安装),由 WShortcutGuide 监听
-        // beforeinstallprompt / appinstalled 维护,给侧边栏/顶部安装入口判断展示。
-        PwaInstalled: 0
+        // PWA 是否已安装(0 未安装、1 已安装)。
+        //   - 主要由 WShortcutGuide 在 outcome==='accepted' 时立刻设 1
+        //   - 同时持久化到 localStorage("pwa_installed"),跨刷新仍认得;
+        //     不能只靠 appinstalled 事件,部分 Android Chrome 不派发它。
+        PwaInstalled: (typeof window!=="undefined" && window.localStorage && window.localStorage.getItem("pwa_installed")==="1") ? 1 : 0
     },
     reducers: {
         logout:(state, action)=>{

+ 52 - 0
src/features/pageSlice.js

@@ -216,6 +216,58 @@ export const getOrderedGames=(state)=>{
     _orderedGamesCache=collectGameItems(pageData);
     return _orderedGamesCache;
 }
+// 「热门游戏」= 页面里第一个含游戏卡片(id/img/link 完整)的模块。
+// 不依赖模块名字 / icon / data_key —— 不管叫什么,排第一就当热门。
+let _hotGamesPageRef=null;
+let _hotGamesCache=[];
+const collectFromNode=(node,out,seen)=>{
+    if (node==null) return;
+    if (Array.isArray(node)){ node.forEach(n=>collectFromNode(n,out,seen)); return; }
+    if (typeof node!=='object') return;
+    if (node.id!=null&&node.img!=null&&node.link!=null){
+        if (!seen.has(node.id)){ seen.add(node.id); out.push(node); }
+    }
+    if (node.data) collectFromNode(node.data,out,seen);
+}
+// 模块自身就是数组 / 顶层就是模块列表 —— 找第一个能挖出游戏卡的模块
+const firstGameModule=(modules)=>{
+    if (!modules) return null;
+    if (!Array.isArray(modules)){
+        // 不是数组,按对象遍历找第一个值
+        for (const k in modules){
+            const found=firstGameModule(modules[k]);
+            if (found) return found;
+        }
+        return null;
+    }
+    for (let i=0;i<modules.length;i++){
+        const m=modules[i];
+        if (m==null) continue;
+        const seen=new Set(); const out=[];
+        collectFromNode(m,out,seen);
+        if (out.length>0) return out;
+    }
+    return null;
+}
+export const getHotGames=(state)=>{
+    const pageData=state.page&&state.page.pageData;
+    if (pageData===_hotGamesPageRef) return _hotGamesCache;
+    _hotGamesPageRef=pageData;
+    let out=[];
+    if (pageData){
+        // pageData 是 { pageId: [modules...] } 结构,取第一个有游戏的模块
+        for (const k in pageData){
+            const list=firstGameModule(pageData[k]);
+            if (list && list.length>0){ out=list; break; }
+        }
+    }
+    if (out.length===0){
+        // 兜底:orderedGames 全量,避免轮播完全空
+        out=collectGameItems(pageData);
+    }
+    _hotGamesCache=out;
+    return _hotGamesCache;
+}
 export const getPopularGames=(state)=>{
     const pageData=state.page&&state.page.pageData;
     const all=collectGameItems(pageData);

+ 11 - 64
src/hooks/usePaymentActions.js

@@ -1,26 +1,23 @@
 import { ROUTE_PATH } from "@constants/routes";
-import { getCPFFirst, getFirstPayGift, getPaymentOrderStatus } from "@features/paySlice";
-import { getUserInfo, isLogined } from "@features/mainSlice";
+import { getFirstPayGift, getPaymentOrderStatus } from "@features/paySlice";
+import { isLogined } from "@features/mainSlice";
 import { useDispatch, useSelector } from "react-redux";
 import { useNavigate } from "react-router";
 import { reqPaymentOrder } from "@features/api";
 import { REQ_STATUS } from "@api/config";
-import { APP_COUNTRY_CODE } from "@constants/app";
+import usePaymentGate from "@hooks/usePaymentGate";
 
 const usePaymentActions = () => {
     const navigate = useNavigate();
     const dispatch = useDispatch();
     const isLogin = useSelector(isLogined);
     const firstPayGift = useSelector(getFirstPayGift);
-    const user = useSelector(getUserInfo);
-    const cpfFirst = useSelector(getCPFFirst);
     const orderStatus = useSelector(getPaymentOrderStatus);
-    const areaItem = global.getLocalAreaItem ? global.getLocalAreaItem() : null;
+    // BR / RU / EU 的下单前置闸门
+    const {runWithGate} = usePaymentGate();
 
     // 统一的下单入口:所有支付按钮(首充、每日礼包、节日活动、彩金等)都走这里。
-    // 之前这里只是 navigate(/pay),导致点击之后用户被甩回到充值页,根本没有
-    // 真正发起订单。现在补全 BR(CPF)/RU(支付方式)/EU(再次确认) 三种地区流程,
-    // 并在成功之后跳转到 PAY_ORDER。
+    // CPF / 支付方式 / EU 二次确认 都由 usePaymentGate 处理。
     const startPayingOrder = (data = { price: 0, goodsName: "", payType: "", GiftsID: 0, gear: "" }) => {
         if (!isLogin) {
             navigate(ROUTE_PATH.SIGN_IN);
@@ -31,61 +28,11 @@ const usePaymentActions = () => {
         // 兼容老调用:保留 global.curGoods,方便其他地方读取当前下单参数
         global.curGoods = data;
 
-        const onGetPaymentCallback = () => {
-            navigate(ROUTE_PATH.PAY_ORDER);
-        };
-
-        const reqOrder = () => {
-            data.user = user;
-            data.callback = onGetPaymentCallback;
-            dispatch(reqPaymentOrder(data)).then((odata) => {
-                if (odata && odata.payload) {
-                    callbackErr(odata.payload);
-                }
-            });
-        };
-
-        const callbackErr = (payload) => {
-            if (payload && payload.code === 301) {
-                if (areaItem && areaItem.code === APP_COUNTRY_CODE.EU) {
-                    if (global.wdepositInfo != null) {
-                        global.wdepositInfo.show(() => {
-                            reqOrder();
-                        });
-                    }
-                }
-            }
-        };
-
-        const code = areaItem ? areaItem.code : null;
-        if (code === APP_COUNTRY_CODE.RU) {
-            if (global.ruPaytypeSelect != null) {
-                global.ruPaytypeSelect.show((eventType) => {
-                    data.eventType = eventType;
-                    reqOrder();
-                });
-            } else {
-                reqOrder();
-            }
-        } else if (code === APP_COUNTRY_CODE.BR) {
-            // 巴西站强制 CPF:未保存过 CPF 时弹窗收集,保存成功后再下单。
-            // global.brCPFSave 现在挂在 Layout 全局,确保任意支付入口都生效。
-            if (cpfFirst) {
-                if (global.brCPFSave != null) {
-                    global.brCPFSave.show(() => {
-                        data.cpf_first = 1;
-                        reqOrder();
-                    });
-                } else {
-                    // 弹窗组件没挂载就别让用户卡死,直接下单兜底
-                    reqOrder();
-                }
-            } else {
-                reqOrder();
-            }
-        } else {
-            reqOrder();
-        }
+        runWithGate({
+            data,
+            dispatchAction: (d) => dispatch(reqPaymentOrder(d)),
+            onSuccessCallback: () => navigate(ROUTE_PATH.PAY_ORDER),
+        });
     };
 
     const getFirstPayGiftInfo = () => {

+ 90 - 0
src/hooks/usePaymentGate.js

@@ -0,0 +1,90 @@
+import {useDispatch, useSelector} from "react-redux";
+import {getUserInfo} from "@features/mainSlice";
+import {getCPFFirst} from "@features/paySlice";
+import {APP_COUNTRY_CODE} from "@constants/app";
+
+// 支付接口前置闸门:BR 必须有 CPF(没填先弹 BRCPFSave),
+// RU 必须先选支付方式(弹 RUPaytypeSelect),EU 拿到 301 时弹 WDepositInfo 二次确认。
+// 任何调用 reqPaymentOrder / reqPaymentOrderQR 的地方都应该经过这里,
+// 这样在新增支付入口时不会再漏掉 CPF / 支付方式校验。
+//
+// 用法:
+//   const {runWithGate} = usePaymentGate();
+//   runWithGate({
+//       data,                                   // 待派发的参数对象(会被原地补 user / cpf_first / eventType)
+//       dispatchAction: (d) => dispatch(reqPaymentOrder(d)),
+//       onSuccessCallback: () => navigate(...), // 可选:放在 data.callback 里
+//   });
+const usePaymentGate = () => {
+    const dispatch = useDispatch();
+    const user = useSelector(getUserInfo);
+    const cpfFirst = useSelector(getCPFFirst);
+    const areaItem = (typeof global.getLocalAreaItem === "function") ? global.getLocalAreaItem() : null;
+
+    const runWithGate = ({data = {}, dispatchAction, onSuccessCallback} = {}) => {
+        if (typeof dispatchAction !== "function") return;
+
+        const fire = () => {
+            data.user = user;
+            if (typeof onSuccessCallback === "function" && data.callback == null) {
+                data.callback = onSuccessCallback;
+            }
+            const ret = dispatchAction(data);
+            // 301 处理:
+            //   EU → 弹 wdepositInfo 二次确认后重发
+            //   BR → 服务端的"请填 CPF"信号,弹 BRCPFSave,保存后带 cpf_first=1 重发
+            //        (这是兜底:本地 cpfFirst 来自 recharge config,可能没刷新过)
+            if (ret && typeof ret.then === "function") {
+                ret.then((odata) => {
+                    const payload = odata && odata.payload;
+                    if (!payload || payload.code !== 301) return;
+                    const code = areaItem ? areaItem.code : null;
+                    if (code === APP_COUNTRY_CODE.EU && global.wdepositInfo != null) {
+                        global.wdepositInfo.show(() => { fire(); });
+                    } else if (code === APP_COUNTRY_CODE.BR && global.brCPFSave != null) {
+                        global.brCPFSave.show(() => {
+                            data.cpf_first = 1;
+                            fire();
+                        });
+                    }
+                });
+            }
+            return ret;
+        };
+
+        const code = areaItem ? areaItem.code : null;
+        if (code === APP_COUNTRY_CODE.RU) {
+            if (global.ruPaytypeSelect != null) {
+                global.ruPaytypeSelect.show((eventType) => {
+                    data.eventType = eventType;
+                    fire();
+                });
+            } else {
+                fire();
+            }
+            return;
+        }
+        if (code === APP_COUNTRY_CODE.BR) {
+            if (cpfFirst) {
+                if (global.brCPFSave != null) {
+                    global.brCPFSave.show(() => {
+                        data.cpf_first = 1;
+                        fire();
+                    });
+                } else {
+                    // BRCPFSave 没挂载时不要让用户卡住,直接发;服务端可能返回错误,再做提示。
+                    fire();
+                }
+            } else {
+                fire();
+            }
+            return;
+        }
+        // 其它区域(MX / US / EU 默认 / 等)直接派发
+        fire();
+    };
+
+    return {runWithGate};
+};
+
+export default usePaymentGate;

+ 12 - 1
src/layout/Navbar/index.jsx

@@ -45,6 +45,13 @@ const i18n=global.i18n;
 const StyledToggleGroup= styled(ToggleButtonGroup)`
   border-radius: 0px !important;
   gap: 12px;
+  // provider 数量较多时不要换行 / 不要把右侧 SignIn 区挤掉,
+  // 给一个横向滚动兜底(一般场景宽度够用就不会出现滚动条)。
+  flex-wrap: nowrap;
+  overflow-x: auto;
+  max-width: 100%;
+  scrollbar-width: thin;
+  &::-webkit-scrollbar { height: 4px; }
 `;
 const StyledToggle=styled(ToggleButton)`
     gap: 12px;
@@ -52,6 +59,8 @@ const StyledToggle=styled(ToggleButton)`
     border: none !important;
     border-radius: var(--border-radius) !important;
     padding: 0px !important;
+    flex: 0 0 auto;
+    white-space: nowrap;
     &:hover, &:focus ,&.Mui-selected, &.active {
       background-color: var(--nav-item-hover) !important;
       a {
@@ -84,7 +93,9 @@ const StyledToggle=styled(ToggleButton)`
 
 const DesktopNavToggle=()=>{
     const LINKS=useSelector(allTopNavs);
-    const links=LINKS.slice(0,4);
+    // 之前固定 slice(0,4) 让顶部导航最多只显示 4 项(Home/Hot Games/PGSoft/PragmaticPlay),
+    // 后面的 provider 全部被截掉。导航栏右侧本身还有大量空白,直接展示全部即可。
+    const links=LINKS;
     const {pathname} = useLocation();
     const [alignment, setAlignment] = useState(pathname);
     const currRoute=useSelector(getRoute(pathname));

+ 6 - 1
src/layout/Routes/RouteContainer.jsx

@@ -18,9 +18,14 @@ const RouteContainer=(props)=>{
         );
     }
     const renderPop=()=>{
+        // 桌面端默认走居中 popup-big;用户信息(#/settings)改成右侧抽屉式 popup-drawer-right。
+        // 注意 route 字段是 component,不是 comp;这里同时兜底用 path 判定更稳。
+        const isSettings = route && (route.path === '#/settings' || route.component === 'settings');
+        const desktopClass = isSettings ? 'popup-drawer-right' : 'popup-big';
+        const popupClass = isMobile() ? '' : desktopClass;
         return(
             <CSSTransition {...props} timeout={500}>
-                <Popup {...props} popupClass={`${isMobile()?'':'popup-big'}`}>
+                <Popup {...props} popupClass={popupClass}>
                     {children}
                 </Popup>
             </CSSTransition>

+ 7 - 1
src/locales/languages/en_US.json

@@ -1,4 +1,7 @@
 {
+  "pwa": {
+    "openHint": "Tap ⋮ in Chrome menu → Open 24680.com"
+  },
   "quickbar": {
     "first_deposit": "1st Deposit",
     "share_earn": "Share & Earn",
@@ -118,7 +121,10 @@
     "withdraw": "Withdraw",
     "deposit": "Deposit",
     "withdrew": "Withdrew",
-    "deposited": "Deposited"
+    "deposited": "Deposited",
+    "tag_win": "Win",
+    "tag_deposit": "Deposit",
+    "tag_withdraw": "Withdraw"
   },
   "small_list": {
     "view_all": "View all"

+ 10 - 2
src/locales/languages/es_MX.json

@@ -1,4 +1,7 @@
 {
+  "pwa": {
+    "openHint": "Toca ⋮ en el menú de Chrome → Abrir 24680.com"
+  },
   "quickbar": {
     "first_deposit": "1er Depósito",
     "share_earn": "Compartir y Ganar",
@@ -118,7 +121,10 @@
     "withdraw": "Retiro",
     "deposit": "Depósito",
     "withdrew": "Retiró",
-    "deposited": "Depositó"
+    "deposited": "Depositó",
+    "tag_win": "Ganancia",
+    "tag_deposit": "Depósito",
+    "tag_withdraw": "Retiro"
   },
   "small_list": {
     "view_all": "Ver todos"
@@ -1460,7 +1466,9 @@
     "tips_1": "Descarga para obtener",
     "tips_2": "{0} BONIFICACIONES",
     "btn_down": "Descargar",
-    "tips_3": "Añadir a la Pantalla de Inicio para ganar"
+    "tips_3": "Añadir a la Pantalla de Inicio para ganar",
+    "open_tip": "Abrir en la app para una mejor experiencia",
+    "btn_open": "Abrir"
   },
   "withdrawPop": {
     "cash_balance": "Saldo en Efectivo",

+ 10 - 2
src/locales/languages/pt_BR.json

@@ -1,4 +1,7 @@
 {
+  "pwa": {
+    "openHint": "Toque em ⋮ no menu do Chrome → Abrir 24680.com"
+  },
   "quickbar": {
     "first_deposit": "1º Depósito",
     "share_earn": "Indicar & Ganhar",
@@ -118,7 +121,10 @@
     "withdraw": "Saque",
     "deposit": "Depósito",
     "withdrew": "Sacou",
-    "deposited": "Depositou"
+    "deposited": "Depositou",
+    "tag_win": "Ganho",
+    "tag_deposit": "Depósito",
+    "tag_withdraw": "Saque"
   },
   "small_list": {
     "view_all": "Ver todos"
@@ -1443,7 +1449,9 @@
     "tips_1": "Baixe agora para ganhar um",
     "tips_2": "BÔNUS de {0}",
     "btn_down": "Download",
-    "tips_3": "Adicionar à Tela Inicial para ganhar"
+    "tips_3": "Adicionar à Tela Inicial para ganhar",
+    "open_tip": "Abrir no app para uma melhor experiência",
+    "btn_open": "Abrir"
   },
   "withdrawPop": {
     "cash_balance": "Saldo em Dinheiro",

+ 26 - 1
src/styles/_elements.scss

@@ -534,13 +534,38 @@ a {
         color: #fff;
     }
     &-big{
-        max-width: 40vw;
+        // 桌面 popup-big:让外框贴合内部内容宽度,而不是固定撑到 40vw。
+        // 用 fit-content 让 popup 按子元素的实际宽度收缩;
+        // max-width 给上限避免极宽内容把框拉太大;min-width 给下限保证不过窄。
+        // 不同弹窗(Userinfo ≈ 32rem / Bonus 38rem / Promotion 40.6rem)会各自贴合。
+        width: fit-content;
+        max-width: min(40vw, 42rem);
         max-height: 90vh;
         min-width: 25rem;
         padding: 2rem 0.5rem 0.5rem 0.5rem;
         overflow-y: auto;
     }
 
+    // 桌面端侧边抽屉式弹窗(用于用户信息 Settings):固定贴右侧,全高显示。
+    // 沿用 popup-window 的圆角/背景/关闭按钮,只覆盖位置与尺寸。
+    &-drawer-right{
+        position: fixed;
+        top: 0;
+        right: 0;
+        bottom: 0;
+        margin: 0;
+        width: fit-content;
+        min-width: 22rem;
+        max-width: min(90vw, 28rem);
+        height: 100vh;
+        max-height: 100vh;
+        border-radius: 1.0625rem 0 0 1.0625rem;
+        padding: 2.5rem 0.75rem 1rem 0.75rem;
+        overflow-y: auto;
+        // MUI Grow 默认从中心缩放;这里改成从右边缘展开,更像抽屉
+        transform-origin: right center !important;
+    }
+
 }
 
 .button-red {

+ 5 - 0
src/styles/_global.scss

@@ -3,6 +3,10 @@
 html {
     user-select: none;
     font-size: calc(100% * var(--font-scale));
+    // 沉浸式 PWA 兜底色:必须等于 --body-dark (#1b1c20),否则 over-scroll 时
+    // Android 会用 html 实际背景色去推 nav bar 着色,又会出现白边
+    background: var(--body, #1b1c20);
+    background-color: #1b1c20;
 
     &.no-transition *:not(.swiper:not(.selection-list) *) {
         transition: none !important;
@@ -15,6 +19,7 @@ html {
 
 body {
     background: var(--body);
+    background-color: #1b1c20; // 兜底,保证未应用 --body 之前也是深色
     color: var(--text);
     font-family: var(--body-font);
     font-size: 0.875rem;

+ 63 - 0
src/utils/PwaDetect.js

@@ -0,0 +1,63 @@
+import {isInPc} from "@utils/UAUtils";
+
+// 已安装探测:综合三个信号,任一为真即认为装过
+//   1. localStorage("isDownload") —— 我们在 accept 后写的标记(最早)
+//   2. navigator.getInstalledRelatedApps() —— 浏览器侧权威,Android Chrome 85+
+//      支持,需要 manifest.related_applications 自指(已在 load.m.js 配上)
+//   3. matchMedia('(display-mode: standalone)') —— 当前已经在 PWA 容器里
+export const detectPwaInstalled = async () => {
+    try {
+        if (JSON.parse(window.localStorage.getItem("isDownload") || "false")) return true;
+    } catch (e) {}
+    try {
+        if (window.matchMedia && window.matchMedia("(display-mode: standalone)").matches) return true;
+    } catch (e) {}
+    try {
+        if ("getInstalledRelatedApps" in navigator) {
+            const apps = await navigator.getInstalledRelatedApps();
+            if (apps && apps.length > 0) return true;
+        }
+    } catch (e) {}
+    return false;
+};
+
+// 启动一个 1s 探测轮询,发现安装就回调一次(自动停止)。
+// 调用方拿 stop() 在卸载时清掉计时器。
+export const startInstalledPolling = (onDetected, intervalMs = 1000) => {
+    let stopped = false;
+    let timer = null;
+    const tick = async () => {
+        if (stopped) return;
+        const yes = await detectPwaInstalled();
+        if (yes) {
+            stopped = true;
+            try { onDetected && onDetected(); } catch (e) {}
+            return;
+        }
+        timer = setTimeout(tick, intervalMs);
+    };
+    tick();
+    return () => {
+        stopped = true;
+        if (timer) clearTimeout(timer);
+    };
+};
+
+// 真正"拉起 PWA"的统一调用点:
+//   桌面:用 manifest 的 web+brslot:// 协议
+//   移动:window.open(href, '_blank', 'pwa=1'),Android Chrome 把 URL
+//        交给已装 PWA 容器打开(不必依赖标准 protocol_handlers)
+// 返回值:是否触发成功(移动端能感知到 popup 是否被允许;桌面只能假定触发)
+export const launchPwa = () => {
+    if (isInPc()) {
+        try {
+            const protocol = (typeof window !== "undefined" && window.PWA_PROTOCOL) || "web+brslot";
+            window.location.href = protocol + "://";
+            return true;
+        } catch (e) { return false; }
+    }
+    try {
+        const w = window.open(window.location.href, "_blank", "pwa=1");
+        return !!w;
+    } catch (e) { return false; }
+};

+ 30 - 0
src/utils/PwaHelp.js

@@ -0,0 +1,30 @@
+import {isInPc, openUrlUseChrome} from "@utils/UAUtils";
+
+// PWA 安装/启动相关的小工具。manifest 的构造放在 public/js/load.m.js 里,
+// 那边会把当前协议写到 window.PWA_PROTOCOL(默认 web+brslot)。
+const getProtocol = () => {
+    const fromWindow = typeof window !== "undefined" ? window.PWA_PROTOCOL : "";
+    return fromWindow && String(fromWindow).length > 0 ? fromWindow : "web+brslot";
+};
+
+// 拉起已安装的 PWA:
+//   桌面:用 manifest 注册的自定义协议(web+brslot://)。Chrome / Edge 会把这种
+//        同域 + 已安装 PWA 范围内的导航识别成 PWA 内启动,能正常拉起。
+//   Android:openUrlUseChrome 内部用隐藏 iframe 触发 intent://...com.android.chrome
+//        的方式,Android IntentResolver 会把请求分发给已注册该 scope 的 PWA,
+//        从而真的拉起 PWA 窗口(直接 window.location 设 intent 不行,会被 Chrome
+//        当成同 tab 导航 —— 这正是 new2 的关键 trick)。
+export const openDesktopApp = () => {
+    if (isInPc()) {
+        const protocol = getProtocol();
+        if (!protocol) return;
+        try { window.location.href = protocol + "://"; } catch(e) {}
+        return;
+    }
+    // 移动端:iframe + intent 拉起已装 PWA
+    try {
+        openUrlUseChrome(window.location.href, true);
+    } catch(e) {
+        try { window.location.reload(); } catch(_) {}
+    }
+};

+ 21 - 1
src/utils/UAUtils.js

@@ -252,6 +252,23 @@ const isShowInstallBox=()=>{
 const openProgressierInstall=()=>{
     if(window.progressier&&window.progressier.flow)window.progressier.flow.init("install",true)
 }
+// 把 intent URL 塞进隐藏 iframe 的 src 里。这是 Android 上从 Chrome 网页拉起
+// 已安装 PWA 的关键 trick:直接 location.href = intent:// 会被 Chrome 当作当前
+// tab 导航处理(PWA 不会被唤起);通过 iframe 子帧触发,Android 的 IntentResolver
+// 会把意图分发给注册了相应 scope 的 PWA(装 PWA 时自动登记),从而启动 PWA 窗口。
+const openUrlInframe=(url)=>{
+    let n = document.getElementById("pwaIFrame");
+    if (n==null){
+        n = document.createElement("iframe");
+        n.id = "pwaIFrame";
+        n.style.width = "0";
+        n.style.height = "0";
+        n.style.opacity = "0";
+        n.name = "pwaIFrame";
+        document.body.appendChild(n);
+    }
+    n.src = url;
+}
 const openUrlUseChrome=(url="")=>{
     try {
         if (url==null||url==="")url=window.location.href;
@@ -267,7 +284,9 @@ const openUrlUseChrome=(url="")=>{
         else if (isAndroid()){
             const r = url.split(":")[0];
             let l = url.replace(/^https?:\/\//, "");
-            window.location.href = "intent://".concat(l, "#Intent;scheme=").concat(r, ";package=com.android.chrome;end")
+            const intent = "intent://".concat(l, "#Intent;scheme=").concat(r, ";package=com.android.chrome;end");
+            // 关键:用 iframe 触发,让 Android IntentResolver 路由到已安装 PWA
+            openUrlInframe(intent);
         }
     }
     catch (e){
@@ -313,6 +332,7 @@ export {
     isIframe,
     isShowInstallBox,
     openUrlUseChrome,
+    openUrlInframe,
     openProgressierInstall,
     getInAppBrowser,
     isInPc,

+ 37 - 8
src/widgets/TopDownload/index.jsx

@@ -6,20 +6,34 @@ import icon from '@assets/topArea/icon.png';
 import { useLocation } from "react-router";
 import { ROUTE_PATH } from "@constants/routes";
 import { checkIsStandalone, formatCash } from "@utils/helpers";
-import { getDownloadConf } from "@features/mainSlice";
+import { getDownloadConf, getPwaInstalled } from "@features/mainSlice";
 import { useSelector } from "react-redux";
 import { useThemeProvider } from "@contexts/themeContext";
 
 const i18n = global.i18n;
 
+// TopDownload 行为:
+//   * 位置始终在底部(不变化)
+//   * 弹安装/扫描相关弹窗时(pushPwaModal) → 隐藏自己;弹窗关 → 重新出来
+//   * 在 PWA 容器里运行(standalone)→ 不显示
+//   * 未装 → 显示 Download 文案
+//   * 已装(pwaInstalled === 1)→ 显示 Open 文案
+//   * 点击都走 global.showShortcutGuide(),由 ShortcutGuide.show 内部判断:
+//       - 还没装 → 弹原生安装确认
+//       - 已装   → window.open(href, '_blank', 'pwa=1') 拉起已装 PWA
 const TopDownload = () => {
     const { isMobile } = useThemeProvider();
     const [show, setShow] = useState(true);
     const [inGame, setInGame] = useState(false);
+    // 弹窗占用屏幕时让 banner 让位
+    const [modalOpen, setModalOpen] = useState(false);
     const isStandalone = checkIsStandalone();
     const { pathname } = useLocation();
 
     const downConf = useSelector(getDownloadConf);
+    const pwaInstalled = useSelector(getPwaInstalled);
+    const installed = pwaInstalled === 1;
+
     const getDownBonus = () => {
         let bonus = Number(downConf?.bonus);
         if (isNaN(bonus)) bonus = 0;
@@ -42,22 +56,37 @@ const TopDownload = () => {
         setShow(isHome);
     }, [pathname, isHome]);
 
+    // 订阅 PWA 弹窗计数:>0 则隐藏 banner(让位给 SecurityScan/QuickInstall/ShortcutGuide)
+    useEffect(() => {
+        if (typeof global.subscribePwaModal !== "function") return;
+        return global.subscribePwaModal((count) => setModalOpen(count > 0));
+    }, []);
+
     if (!isHome) return <></>;
     if (!isMobile() || !show || inGame || isStandalone || window.iosapp) return <></>;
+    if (modalOpen) return <></>;
+
     return (
         <div className={styles.container} onClick={handleClick}>
             <div className={styles.content}>
                 <img className={styles.close} src={close} onClick={handleClickClose} />
                 <img className={styles.icon} src={icon} />
-                <div className={styles.title}>
-                    <span>{i18n.t('topDownload.title')}</span>
-                    <div>
-                        <span>{i18n.t('topDownload.bonus')}</span>
-                        <span className={styles.bonus}> {getDownBonus()}</span>
+                {installed ? (
+                    // 已安装:单行 Open 提示,文字简洁,按钮文案改 Abrir
+                    <div className={styles.title}>
+                        <span>{i18n.t('topdown.open_tip')}</span>
                     </div>
-                </div>
+                ) : (
+                    <div className={styles.title}>
+                        <span>{i18n.t('topDownload.title')}</span>
+                        <div>
+                            <span>{i18n.t('topDownload.bonus')}</span>
+                            <span className={styles.bonus}> {getDownBonus()}</span>
+                        </div>
+                    </div>
+                )}
                 <div className={styles.downloadBtn}>
-                    {i18n.t('topDownload.btn')}
+                    {installed ? i18n.t('topdown.btn_open') : i18n.t('topDownload.btn')}
                 </div>
             </div>
         </div>

+ 0 - 5
src/widgets/TopDownload/styles.module.scss

@@ -1,12 +1,7 @@
 .container {
-    // width: 100%;
-    // height: auto;
-    // background-color: #009553;
     position: fixed;
     bottom: 7.35rem;
-    // bottom: 0;
     z-index: 1000;
-    // top: 0;
 
     width: calc(100% - 1.5rem);
     max-width: 24.63rem;

+ 2 - 1
src/widgets/WBonus/styles.module.scss

@@ -51,7 +51,8 @@
                 // 让卡片始终撑满父容器宽度(桌面 popup-big 也能铺满),
                 // 用 aspect-ratio 维持原 25.36 : 7.279 ≈ 3.48 的比例。
                 width: 100%;
-                max-width: 40.6rem; // 桌面给一个上限(≈ 25.36rem * 1.6),避免极宽屏拉太大
+                // 桌面给上限:移动端原宽 ≈ 25.36rem,1.5x ≈ 38rem,避免桌面拉太大空。
+                max-width: 38rem;
                 aspect-ratio: 25.36 / 7.279;
                 height: auto;
 

+ 8 - 47
src/widgets/WBonusPack/index.jsx

@@ -16,6 +16,7 @@ import {useNavigate} from "react-router-dom";
 import RUPaytypeSelect from "@widgets/WDeposit/RU/RUPaytypeSelect";
 import BRCPFSave from "@widgets/WDeposit/BR/BRCPFSave/BRCPFSave";
 import {formatTime} from "@utils/helpers";
+import usePaymentGate from "@hooks/usePaymentGate";
 const i18n=global.i18n;
 const WBonusPack = () => {
     const [isShow, setIsShow] = useState(false);
@@ -28,6 +29,7 @@ const WBonusPack = () => {
     const navigate=useNavigate();
     const cpfFirst=useSelector(getCPFFirst);
     const areaItem=global.getLocalAreaItem();
+    const {runWithGate}=usePaymentGate();
     const show=()=>{
         setIsShow(true);
         setWelfTabState(APP_WELFARE.BONUS_PACK);
@@ -40,6 +42,7 @@ const WBonusPack = () => {
         hide();
         navigate(ROUTE_PATH.PAY_ORDER);
     }
+    // 统一走 usePaymentGate:BR-CPF / RU-paytype / EU-301 都封装其中
     const onGetPaymentOder=(data)=>{
         if (!isLogin){
             hide();
@@ -47,53 +50,11 @@ const WBonusPack = () => {
             return;
         }
         if (orderStatus===REQ_STATUS.LOADING)return;
-        let reqOrder=()=>{
-            data.user=user;
-            data.callback=onGetPaymentCallback;
-            // data.callbackError=callbackErr;
-            // dispatch(reqPaymentOrder(data));
-            dispatch(reqPaymentOrder(data)).then((odata)=>{
-                if (odata.payload){
-                    callbackErr(odata.payload);
-                }
-            });
-        }
-        let callbackErr=(payload)=>{
-            if (payload&&payload.code===301){
-                if (areaItem.code===APP_COUNTRY_CODE.EU){
-                    if (global.wdepositInfo!=null){
-                        global.wdepositInfo.show(()=>{
-                            reqOrder();
-                        });
-                    }
-                }
-            }
-        }
-        if (areaItem.code===APP_COUNTRY_CODE.RU){
-            if (global.ruPaytypeSelect!=null){
-                global.ruPaytypeSelect.show((eventType)=>{
-                    data.eventType=eventType;
-                    reqOrder();
-                });
-            }
-        }
-        else if (areaItem.code===APP_COUNTRY_CODE.BR){
-            if (cpfFirst){
-                if (global.brCPFSave!=null){
-                    global.brCPFSave.show(()=>{
-                        data.cpf_first=1;
-                        reqOrder();
-                    })
-                }
-            }
-            else
-            {
-                reqOrder();
-            }
-        }
-        else{
-            reqOrder();
-        }
+        runWithGate({
+            data,
+            dispatchAction:(d)=>dispatch(reqPaymentOrder(d)),
+            onSuccessCallback:onGetPaymentCallback,
+        });
     }
     useEffect(()=>{
         const bonusPack={

+ 8 - 46
src/widgets/WDeposit/BR/BRDeposit.jsx

@@ -23,6 +23,7 @@ import img_bonus from "@assets/pay/topup_bonus.png";
 import styled from 'styled-components/macro';
 import {formatCurrency, formatTime, stringFormat, trimZeroDecimals} from "@utils/helpers";
 import usePaymentActions from "@hooks/usePaymentActions";
+import usePaymentGate from "@hooks/usePaymentGate";
 import {APP_COUNTRY_CODE} from "@constants/app";
 import RUPaytypeSelect from "@widgets/WDeposit/RU/RUPaytypeSelect";
 import BRCPFSave from "@widgets/WDeposit/BR/BRCPFSave/BRCPFSave";
@@ -76,6 +77,7 @@ const BRDeposit=()=>{
     const firstPayMulti=useSelector(getFirstPayMulti);
     const getSecondStatus=useSelector(getGetSecondStatus);
     const {getFirstPayGiftInfo}=usePaymentActions();
+    const {runWithGate}=usePaymentGate();
     const firstPayGiftInfo=getFirstPayGiftInfo();
     const getSelectedConfig=()=>{
         let selected=null;
@@ -93,58 +95,18 @@ const BRDeposit=()=>{
     const onGetPaymentCallback=()=>{
         navigate(ROUTE_PATH.PAY_ORDER);
     }
+    // 改为统一走 usePaymentGate:BR/RU/EU 的前置弹窗 + EU 301 重发都封装在那
     const onGetPaymentOder=(data)=>{
         if (!isLogin){
             navigate(ROUTE_PATH.SIGN_IN);
             return;
         }
         if (orderStatus===REQ_STATUS.LOADING)return;
-        let reqOrder=()=>{
-            data.user=user;
-            data.callback=onGetPaymentCallback;
-            // data.callbackError=callbackErr;
-            dispatch(reqPaymentOrder(data)).then((odata)=>{
-                if (odata.payload){
-                    callbackErr(odata.payload);
-                }
-            });
-        }
-        let callbackErr=(payload)=>{
-            if (payload&&payload.code===301){
-                if (areaItem.code===APP_COUNTRY_CODE.EU){
-                    if (global.wdepositInfo!=null){
-                        global.wdepositInfo.show(()=>{
-                            reqOrder();
-                        });
-                    }
-                }
-            }
-        }
-        if (areaItem.code===APP_COUNTRY_CODE.RU){
-            if (global.ruPaytypeSelect!=null){
-                global.ruPaytypeSelect.show((eventType)=>{
-                    data.eventType=eventType;
-                    reqOrder();
-                });
-            }
-        }
-        else if (areaItem.code===APP_COUNTRY_CODE.BR){
-            if (cpfFirst){
-                if (global.brCPFSave!=null){
-                    global.brCPFSave.show(()=>{
-                        data.cpf_first=1;
-                        reqOrder();
-                    })
-                }
-            }
-            else
-            {
-                reqOrder();
-            }
-        }
-        else{
-            reqOrder();
-        }
+        runWithGate({
+            data,
+            dispatchAction:(d)=>dispatch(reqPaymentOrder(d)),
+            onSuccessCallback:onGetPaymentCallback,
+        });
     }
     const getGiftImg=()=>{
         try {

+ 1 - 1
src/widgets/WEditProfile/index.jsx

@@ -28,7 +28,7 @@ const TYPE_PHONE="phone";
 const TYPE_EMAIL="email";
 const SelectAvatar=({innerRef, ...props})=>{
     const [value, setValue] = useState(props.value);
-    const aNum=16;
+    const aNum=15; // CDN 上只有 1..15,第 16 个返回 404
     const avatars = Array.from({ length:aNum }, (value, index) => index + 1);
     return (
         <div className={styles.select_avatar_con}

+ 65 - 3
src/widgets/WGame/WDGame/WDGame.jsx

@@ -2,10 +2,11 @@
 import Spring from "@components/Spring";
 import styles from "./styles.module.scss";
 import {LOCAL_GAME_INFO, LOCAL_GAME_URL, LOCAL_SIGN} from "@constants/localkey";
+import {ROUTE_PATH} from "@constants/routes";
 import {localStorageGet, openFullScreen} from "@utils/helpers";
 import React, {useEffect, useState} from "react";
 import {useDispatch, useSelector} from "react-redux";
-import {getGameCardInfo, getGameLike, getRoute, isLogined, updateGameLike} from "@features/mainSlice";
+import {getGameCardInfo, getGameLike, getRoute, isLogined, updateGameLike, updateGameCardInfo} from "@features/mainSlice";
 import BreadCrumb from "@layout/BreadCrumb";
 import {useThemeProvider} from "@contexts/themeContext";
 import {getHallConnState} from "@features/socketSlice";
@@ -14,15 +15,20 @@ import SmallLoading from "@components/SmallLoading";
 import SvgIcon from "@components/SvgIcon";
 import useGameIFrame from "@hooks/useGameIFrame";
 import MTooltip from "@components/MTooltip";
-import {updatePageGameLike} from "@features/pageSlice";
+import {updatePageGameLike, getHotGames} from "@features/pageSlice";
 import {reqGameCardInfo, reqUpdateFavoInfo} from "@features/api";
 import Popup from "@components/Popup";
 import LoadingIn from "@components/Loading/LoadingIn";
+import LazyImgix from "@components/Imgix/LazyImgix";
+import {useNavigate} from "react-router-dom";
+import {Swiper, SwiperSlide} from "swiper/react";
+import {Autoplay} from "swiper/modules";
 const i18n=global.i18n;
 
 const WDGame=()=>{
     // const {pathname}=useLocation();
     const {preLocation,gameID} = useThemeProvider();
+    const navigate = useNavigate();
     const gameid=gameID;
     const gameCardInfo=useSelector(getGameCardInfo(gameid))
     const [gameURL, setGameURL] = useState("");
@@ -33,6 +39,8 @@ const WDGame=()=>{
     const {isGameLoad,setIsGameLoad,iFrameRef}=useGameIFrame();
     const [movieMode,setMovieMode]=useState(false);
     const isLike=useSelector(getGameLike(gameID));
+    // 推荐游戏池:直接复用首屏第一个 GameList,不依赖具体 module id/title。
+    const hotGames=useSelector(getHotGames);
     const dispatch = useDispatch();
     const isLogin=useSelector(isLogined);
     const [openShare,setOpenShare]=useState(false);
@@ -137,6 +145,21 @@ const WDGame=()=>{
     const onChangeMovieMode=()=>{
         setMovieMode(!movieMode);
     }
+    // 点击推荐游戏:清掉旧 iframe 状态、写入新游戏卡缓存、拉详情、跳路由
+    const handleOpenGame = (data) => {
+        if (!data || data.id == null) return;
+        if (Number(data.id) === Number(gameID)) return;
+        setIsGameLoad(false);
+        setOpenShare(false);
+        setGameURL("");
+        if (typeof updateGameCardInfo === "function") {
+            dispatch(updateGameCardInfo({data, level: 0}));
+        }
+        dispatch(reqGameCardInfo(data.id));
+        const path = (ROUTE_PATH && ROUTE_PATH.GAME ? ROUTE_PATH.GAME : "/game") + "/" + data.id;
+        global.authLockPath = path;
+        navigate(path);
+    }
     const handleLike=()=>{
         let info={id:gameID,data:gameInfo,like:(1-isLike)}
         dispatch(updateGameLike(info));
@@ -248,6 +271,44 @@ const WDGame=()=>{
             </div>
         )
     }
+    // 桌面正常模式(非 Movie Mode)下,游戏 iframe 下方显示热门推荐
+    const renderRecommendedGames = () => {
+        const list = (hotGames || [])
+            .filter((it) => it && Number(it.id) !== Number(gameID))
+            .slice(0, 16);
+        if (list.length === 0) return (<></>);
+        return (
+            <div className={styles.recommend_wrap}>
+                <div className={styles.recommend_title}>{i18n.t("gameMenu.most_popular")}</div>
+                <Swiper
+                    modules={[Autoplay]}
+                    breakpoints={{
+                        0:    { slidesPerView: 3.2, spaceBetween: 10 },
+                        1024: { slidesPerView: 5.5, spaceBetween: 12 },
+                        1440: { slidesPerView: 6.5, spaceBetween: 12 },
+                        1800: { slidesPerView: 7.5, spaceBetween: 14 }
+                    }}
+                    spaceBetween={12}
+                    loop
+                    speed={550}
+                    slidesPerGroup={1}
+                    allowTouchMove={true}
+                    autoplay={{delay: 3200, disableOnInteraction: false, pauseOnMouseEnter: true}}
+                    className={styles.recommend_swiper}
+                >
+                    {
+                        list.map((item, index) => (
+                            <SwiperSlide key={index}>
+                                <button className={styles.recommend_item} onClick={() => handleOpenGame(item)}>
+                                    <LazyImgix src={item.img} alt={item.title || "game"} width={320}/>
+                                </button>
+                            </SwiperSlide>
+                        ))
+                    }
+                </Swiper>
+            </div>
+        )
+    }
     const renderShareBox=()=>{
         return (
             <Popup onClose={()=>{
@@ -279,9 +340,10 @@ const WDGame=()=>{
             {
                 gameCardInfo==null?<LoadingIn></LoadingIn>:(
                     <Spring type={"slideUp"}>
-                        <div className={`${styles.container} page-max-width-wrap`}>
+                        <div className={`${styles.container} ${movieMode ? '' : 'page-max-width-wrap'}`}>
                             {renderTopNavBar()}
                             {renderGame()}
+                            {!movieMode && renderRecommendedGames()}
                             {renderShareBox()}
                         </div>
                     </Spring>

+ 61 - 2
src/widgets/WGame/WDGame/styles.module.scss

@@ -26,8 +26,11 @@
   //width: 100%;
   transition: all .3s;
   &.movie_mode{
-    margin-left: -1.875rem;
-    margin-right: -1.875rem;
+    // Movie Mode:靠 WDGame 容器去掉 page-max-width-wrap 来释放 1430px 限制,
+    // 这里只控制内部的水平安全边距(避免贴到 sidebar / 滚动条)。
+    width: 100%;
+    padding-left: 12px;
+    padding-right: 12px;
   }
 }
 .game_action_wrap{
@@ -90,9 +93,22 @@
 .game_main{
   position: relative;
   width: 100%;
+  // 默认按 1624:750 比例,但加最小高度,避免在 16:9/16:10 桌面上 iframe 太矮、
+  // 下面留一大片空白;min 取 72vh,并夹在 420px ~ 860px 之间。
   aspect-ratio: 1624/750;
+  min-height: clamp(420px, 72vh, 860px);
+  max-height: calc(100vh - 200px);  // 顶部导航 + 面包屑 + 底部工具条 安全余量
   background-color: var(--body);
 }
+// Movie Mode:放弃比例约束,按视口高度撑开 iframe,但留出顶部 navbar(~64) +
+// 面包屑(~38) + 底部工具条(~54) + 上下安全余量(~50),整体扣 ~200px,
+// 避免出现页面纵向滚动条。
+.movie_mode .game_main{
+  aspect-ratio: auto;
+  height: calc(100vh - 200px);
+  max-height: calc(100vh - 200px);
+  min-height: 480px;
+}
 .game_iframe_wrap{
   position:relative;
   width: 100%;
@@ -152,6 +168,49 @@
     opacity: .75
   }
 }
+/* 桌面正常模式下,游戏 iframe 下方的"MOST POPULAR"推荐游戏轮播 */
+.recommend_wrap{
+  display: flex;
+  flex-direction: column;
+  gap: 0.75rem;
+  width: 100%;
+  padding-top: 0.75rem;
+  flex-shrink: 0;
+}
+.recommend_title{
+  color: var(--header);
+  font-size: 1rem;
+  font-weight: 700;
+  letter-spacing: 0.04em;
+}
+.recommend_swiper{
+  width: 100%;
+  :global(.swiper-slide){
+    height: auto;
+  }
+}
+.recommend_item{
+  display: block;
+  width: 100%;
+  aspect-ratio: 384 / 516;
+  border-radius: 0.5rem;
+  overflow: hidden;
+  background: var(--darkColor1, #272D39);
+  border: 0;
+  padding: 0;
+  cursor: pointer;
+  transition: transform .18s ease, box-shadow .18s ease;
+  img{
+    display: block;
+    width: 100%;
+    height: 100%;
+    object-fit: cover;
+  }
+  &:hover{
+    transform: translateY(-2px);
+    box-shadow: 0 6px 18px rgba(0,0,0,0.45);
+  }
+}
 .share_box{
   width: 100%;
   height: 100%;

+ 22 - 2
src/widgets/WInstallBox/WQuickInstall/index.jsx

@@ -6,13 +6,16 @@ import Modal from "@mui/material/Modal";
 import ic_raid_install from "@assets/ggsecurity/ic_raid_install.webp";
 import ic_actived from "@assets/ggsecurity/ic_actived.webp";
 import {ReactComponent as ArcIcon} from "@assets/ggsecurity/arc.svg";
-import {openUrlUseChrome} from "@utils/UAUtils";
+import {launchPwa, startInstalledPolling} from "@utils/PwaDetect";
+import {useDispatch} from "react-redux";
+import {updatePwaInstalled} from "@features/mainSlice";
 const objInfo={};
 const i18n=global.i18n;
 const WQuickInstall = () => {
     const [isShow, setIsShow] = useState(false);
     const [progress, setProgress] = useState(0);
     const isComplete = progress >= 100;
+    const dispatch = useDispatch();
     const show=()=>{
         setIsShow(true);
     }
@@ -27,7 +30,14 @@ const WQuickInstall = () => {
     }
     const onClickInstall=()=>{
         hide();
-        openUrlUseChrome(window.location.href,true);
+        // 用户看完整套等待动画(~20s),PWA 早就注册好,清掉"刚装完"标记
+        global.isCurrentPwaInstall = false;
+        // launchPwa 内部按平台分流:桌面用 protocol,移动用 window.open(pwa=1)
+        const launched = launchPwa();
+        if (!launched) {
+            // 极端兜底:刷新当前页,至少让 banner 切回 Open 模式
+            try { window.location.reload(); } catch(_) {}
+        }
     }
     useEffect(()=>{
         global.quickInstall={
@@ -39,7 +49,9 @@ const WQuickInstall = () => {
         }
     },[]);
     useEffect(()=>{
+        let stopPolling = null;
         if (isShow){
+            if (typeof global.pushPwaModal === "function") global.pushPwaModal();
             let progressValue = 0;
             setProgress(progressValue);
             objInfo.interval = setInterval(() => {
@@ -51,10 +63,18 @@ const WQuickInstall = () => {
                 progressValue += 1;
                 setProgress(progressValue);
             }, 100); // 每100ms增加1%,总共10秒
+            // 1s 轮询:探到已装就同步 Redux + 解锁 isCurrentPwaInstall
+            stopPolling = startInstalledPolling(() => {
+                global.isCurrentPwaInstall = false;
+                try { dispatch(updatePwaInstalled(1)); } catch(e){}
+                try { window.localStorage.setItem("pwa_installed", "1"); } catch(e){}
+            }, 1000);
         }
         else clearProgressTimer();
         return ()=>{
             clearProgressTimer();
+            if (stopPolling) stopPolling();
+            if (isShow && typeof global.popPwaModal === "function") global.popPwaModal();
         }
     },[isShow])
     const renderActived =()=>{

+ 28 - 1
src/widgets/WInstallBox/WSecurityScan/index.jsx

@@ -10,6 +10,9 @@ import safe from "@assets/ggsecurity/safe.webp";
 import done from "@assets/ggsecurity/done.webp";
 import LazyImage from '@components/LazyImage';
 import {getMainDomain} from "@utils/helpers";
+import {startInstalledPolling} from "@utils/PwaDetect";
+import {useDispatch} from "react-redux";
+import {updatePwaInstalled} from "@features/mainSlice";
 const objInfo={};
 const i18n=global.i18n;
 const SCAN_ITEMS=[
@@ -35,6 +38,7 @@ const WSecurityScan=() => {
     const [progress, setProgress] = useState(0);
     const isComplete = progress >= 100;
     const iconUrl=window.themeConfig?.icon || "/resources/icons/icon-192.png";
+    const dispatch = useDispatch();
     const show=()=>{
         setIsShow(true);
     }
@@ -48,7 +52,13 @@ const WSecurityScan=() => {
         }
     }
     const onClickOpen=()=>{
+        // 扫描没结束时不允许点(按钮会 disabled,这里再兜底一次)
+        if (!isComplete) return;
         hide();
+        // 中间窗口不主动 launchPwa(),否则 window.open(pwa=1) 在某些 Android
+        // Chrome 上会被当成普通新标签页打开,反而看上去"什么都没拉起"。
+        // 真正发起拉起的只放在最后一步 WQuickInstall.onClickInstall。
+        global.isCurrentPwaInstall = false;
         global.showQuickInstall();
     }
     useEffect(()=>{
@@ -61,7 +71,10 @@ const WSecurityScan=() => {
         }
     },[]);
     useEffect(()=>{
+        let stopPolling = null;
         if (isShow){
+            // 通知 TopDownload 隐藏自己,避免重叠
+            if (typeof global.pushPwaModal === "function") global.pushPwaModal();
             let progressValue = 0;
             setProgress(progressValue);
             objInfo.interval = setInterval(() => {
@@ -73,10 +86,19 @@ const WSecurityScan=() => {
                 progressValue += 1;
                 setProgress(progressValue);
             }, 100); // 每100ms增加1%,总共10秒
+            // 1s 轮询:一旦探测到已装 → 标 Redux + 清"刚装完"flag,
+            // 让用户点 Open 时能直接 launchPwa 成功
+            stopPolling = startInstalledPolling(() => {
+                global.isCurrentPwaInstall = false;
+                try { dispatch(updatePwaInstalled(1)); } catch(e){}
+                try { window.localStorage.setItem("pwa_installed", "1"); } catch(e){}
+            }, 1000);
         }
         else clearProgressTimer();
         return ()=>{
             clearProgressTimer();
+            if (stopPolling) stopPolling();
+            if (isShow && typeof global.popPwaModal === "function") global.popPwaModal();
         }
     },[isShow])
     const renderBg=()=>{
@@ -159,9 +181,14 @@ const WSecurityScan=() => {
         )
     }
     const renderOpenButton=()=>{
+        // 扫描进行中按钮 disabled,避免用户秒点跳过等待动画
         return (
             <div className={styles.button}>
-                <button onClick={onClickOpen}>{i18n.t("securityScan.btn_open")}</button>
+                <button onClick={onClickOpen}
+                        disabled={!isComplete}
+                        className={!isComplete ? styles.btn_disabled : ""}>
+                    {i18n.t("securityScan.btn_open")}
+                </button>
             </div>
         )
     }

+ 28 - 1
src/widgets/WInstallBox/WSecurityScan/styles.module.scss

@@ -57,18 +57,38 @@
     }
     & .logo_box{
       display: flex;
+      align-items: flex-start;
+      gap: 0.73rem;
+      // 让右侧 title 在容器内自由换行/截断,不挤压左侧图标
+      & > div:last-child{
+        flex: 1 1 auto;
+        min-width: 0;
+      }
       & .logo{
         width: 5.13rem;
         height: 5.13rem;
         border-radius: 1rem;
-        margin-right: 0.73rem;
+        // margin-right 用 gap 替代,gap 在 flex 里更可靠
+        flex: 0 0 5.13rem;     // 关键:禁止收缩
         overflow: hidden;
+        img{
+          width: 100%;
+          height: 100%;
+          object-fit: cover;
+          display: block;
+        }
       }
       & .right_title{
         color: #464646;
         font-size: 1.32rem;
         line-height: 1.4;
         text-transform: capitalize;
+        // 长标题最多两行,超出省略,避免把布局撑爆
+        display: -webkit-box;
+        -webkit-line-clamp: 2;
+        -webkit-box-orient: vertical;
+        overflow: hidden;
+        word-break: break-word;
       }
       & .right_percentage{
         color: #8a8a8a;
@@ -139,6 +159,13 @@
         height: 2.93rem;
         outline: none;
         width: 100%;
+        cursor: pointer;
+        transition: background 0.2s, opacity 0.2s;
+        &:disabled, &.btn_disabled{
+          background: #a3a3a3;
+          cursor: not-allowed;
+          opacity: 0.7;
+        }
       }
     }
   }

+ 51 - 23
src/widgets/WInstallBox/WShortcutGuide/index.jsx

@@ -1,5 +1,5 @@
 import { useEffect, useState, useRef } from "react";
-import {checkIsStandalone, formatCash, isAndroid, stringFormat} from "@utils/helpers";
+import {checkIsStandalone, formatCash, isAndroid, isIOS, localStorageGet, localStorageSet, stringFormat} from "@utils/helpers";
 import MuiDrawer from "@mui/material/Drawer";
 import styles from "./styles.module.scss";
 import img_hand from "@assets/shortcut/pointer.avif";
@@ -7,6 +7,7 @@ import { reqGetRoutes } from "@features/api";
 import {useDispatch, useSelector} from "react-redux";
 import {getDownloadConf, updatePwaInstalled} from "@features/mainSlice";
 import {iosVersion, isChrome, isInPc} from "@utils/UAUtils";
+import {launchPwa, detectPwaInstalled} from "@utils/PwaDetect";
 //新增
 import img_close from "@assets/shortcut/close.webp";
 import img_add from "@assets/shortcut/add.png";
@@ -80,46 +81,65 @@ const WShortcutGuide = () => {
 
         event.prompt();
         // 原生安装弹窗弹出后,覆盖一层"安全扫描"过场,避免空白等待。
-        // 用户在系统弹窗里点 Install / Cancel 都会回到我们的 SecurityScan 上面,
-        // 给 PWA 实际安装/拉资源争取时间,scan 完后还有 QuickInstall 假进度兜底。
         if (isAndroid() && typeof global.showSecurityScan === "function") {
             global.showSecurityScan();
         }
         const { outcome } = await event.userChoice;
-        deferredPromptRef.current = null; // 清空,beforeinstallprompt 不会重复触发
+        deferredPromptRef.current = null;
         if (outcome === 'accepted') {
-            window.addEventListener("appinstalled",handleDownloadSuccess);
+            handleDownloadSuccess();
+            // 立即更新 Redux + localStorage,pwaInstalled 跨刷新都能记住
+            try { window.localStorage.setItem("pwa_installed", "1"); } catch(e){}
+            dispatch(updatePwaInstalled(1));
+            // 跟 /new 一致:装好后顺手刷新一次 routes
+            try { dispatch(reqGetRoutes()); } catch(e){}
         }
 
         setIsShow(false);
     };
 
-    const handleDownloadSuccess = (e) => {
-        if (isInPc()){
-            setTimeout(()=>{
-                dispatch(reqGetRoutes());
-            },1000)
+    // /new 的关键标记:
+    //   isDownload (localStorage):跨页面记住"这个浏览器装过 PWA"
+    //   isCurrentPwaInstall (window 全局):当前 session 刚装完,PWA 还没就绪,
+    //     先别去 window.open(pwa=1) 拉,否则会失败;下次刷新这个标记自动消失。
+    const handleDownloadSuccess = () => {
+        try { localStorageSet("isDownload", true); } catch(e){}
+        global.isCurrentPwaInstall = true;
+    };
+
+    // 已装的核心拉起:用 launchPwa 统一处理(桌面 protocol、移动 window.open(pwa=1))。
+    // 没装 / 当前 session 刚装完 / 不支持 → 退到自定义引导。
+    const openPWA = async () => {
+        // 多源探测:localStorage flag / standalone / getInstalledRelatedApps
+        const detected = await detectPwaInstalled();
+        if (detected && !global.isCurrentPwaInstall) {
+            const ok = launchPwa();
+            if (ok) return;
         }
-        else {
-            setTimeout(()=>{
-                dispatch(updatePwaInstalled(1));
-            },8000)
+        // 没装过或刚装完:走非 PWA 的引导
+        if (isAndroid()) {
+            if (typeof global.showSecurityScan === "function") global.showSecurityScan();
+            else setIsShow(true);
+        } else {
+            setIsShow(true); // iOS / 桌面 → 自定义引导
+            if (isIOS() && !checkIsStandalone()) {
+                // 桌面启动加一个永久标记,便于站点识别
+                try {
+                    const url = new URL(window.location.href);
+                    url.searchParams.set("launchedfrom", "homescreen");
+                    window.history.replaceState(null, "", url.toString());
+                } catch(e){}
+            }
         }
-        window.removeEventListener("appinstalled",handleDownloadSuccess);
     };
 
     const show = () => {
         if (deferredPromptRef.current && isAndroid()) {
-            // 安卓 Chrome 系,直接调起浏览器自带的安装确认
+            // 还没装:弹 Chrome 原生安装确认
             handleClick();
         } else {
-            if (isAndroid()){
-                // 没拿到 deferredPrompt(可能是不支持 PWA 的浏览器,或者已经安装过),
-                // 走 SecurityScan 引导(如果项目里有挂载);没有就退化为我们的自定义引导。
-                if (typeof global.showSecurityScan === "function") global.showSecurityScan();
-                else setIsShow(true);
-            }
-            else setIsShow(true); // iOS / 桌面 → 自定义引导
+            // 已装 / 不支持 deferredPrompt → 尝试 openPWA
+            openPWA();
         }
     };
 
@@ -133,6 +153,14 @@ const WShortcutGuide = () => {
             objInfo.count = 0;
         }
     };
+    // isShow 切换时,通知 TopDownload 显示/隐藏自己
+    useEffect(() => {
+        if (!isShow) return;
+        if (typeof global.pushPwaModal === "function") global.pushPwaModal();
+        return () => {
+            if (typeof global.popPwaModal === "function") global.popPwaModal();
+        };
+    }, [isShow]);
     useEffect(() => {
         const shortcutGuide = {
             show,

+ 11 - 3
src/widgets/WPaymentOrder/MX/MXPayOrder.jsx

@@ -16,6 +16,7 @@ import qrlogo from "@assets/pay/mx/qrlogo.png";
 import {REQ_STATUS} from "@api/config";
 import {reqPaymentOrderQR} from "@features/api";
 import {getUserInfo} from "@features/mainSlice";
+import usePaymentGate from "@hooks/usePaymentGate";
 const i18n=global.i18n;
 const MXPayOrder = ({close}) => {
     const orderInfo=useSelector(getCurrPayOrder);
@@ -24,6 +25,9 @@ const MXPayOrder = ({close}) => {
     const qrstatus=useSelector(getPaymentOrderQRStatus);
     const dispatch = useDispatch();
     const user=useSelector(getUserInfo);
+    // 支付前置闸门:BR-CPF / RU-paytype / EU-301 都统一走这一层。
+    // MX 屏理论上是墨西哥的 QR 重生成,但在通用闸门下加保险,BR 万一走到也不漏。
+    const {runWithGate}=usePaymentGate();
     const getLeftTime=()=>{
         if (orderInfo==null)return 0;
         let deadline=Number(orderInfo.deadline);
@@ -70,9 +74,13 @@ const MXPayOrder = ({close}) => {
         if (qrstatus===REQ_STATUS.LOADING)return;
         if (global.loadingCircular)global.loadingCircular.show(true);
         let data={...orderInfo.arg,user,callback};
-        dispatch(reqPaymentOrderQR(data)).then((respose)=>{
-            if (global.loadingCircular)global.loadingCircular.hide();
-        })
+        runWithGate({
+            data,
+            dispatchAction:(d)=>dispatch(reqPaymentOrderQR(d)).then((respose)=>{
+                if (global.loadingCircular)global.loadingCircular.hide();
+                return respose;
+            }),
+        });
     }
     const renderCopyAll=()=>{
         let content=i18n.t("deposit.order_info_mx.title1")+":"+orderInfo.bank+"\n";

+ 9 - 3
src/widgets/WPromotion2/styles.module.scss

@@ -44,8 +44,13 @@
             .act_item {
                 margin-bottom: 0.73rem;
                 width: 100%;
-                height: 15.4rem;
-                height: 11.7rem;
+                // 桌面 popup-big 容器变宽时,原先固定 11.7rem 高会让横幅图被裁掉。
+                // 学 WBonus 的写法用 aspect-ratio 等比缩放;max-width 限制最多放到
+                // 移动端原尺寸的 1.5x(移动端宽 ≈ 11.7rem * 1350/583 ≈ 27.1rem,
+                // 1.5x ≈ 40.6rem),避免桌面把图拉得过大。
+                max-width: 40.6rem;
+                aspect-ratio: 1350 / 583;
+                height: auto;
                 position: relative;
                 margin-bottom: 4.43rem;
 
@@ -61,11 +66,12 @@
 
                 .banner {
                     width: 100%;
-                    height: 11.7rem;
+                    height: 100%;
                     position: absolute;
                     top: 0;
                     left: 0;
                     border-radius: 0.73529rem 0.73529rem 0 0;
+                    object-fit: cover;
                 }
 
                 .title {

+ 4 - 3
src/widgets/WWithdraw/BR/BRWithdraw.jsx

@@ -97,10 +97,11 @@ const BRWithdraw=()=>{
         if (lnum<=0){
             return;
         }
-        let num=Number(wdValue);
-        if (num>0){
+        // 后端 score 用最小单位(分),需要 *CASH_BASE;
+        // 之前直接传 Number(wdValue) 少乘了 100,导致提现金额偏小。
+        if (inputNum>0){
             dispatch(reqWithdrawPayout({
-                score:num
+                score:inputNum
             }))
         }
     }