浏览代码

no message

PPPPPPP 5 月之前
父节点
当前提交
cb1e83d909

+ 18 - 21
public/index.html

@@ -5,30 +5,27 @@
     <!-- <link rel="icon" href="%PUBLIC_URL%/favicon.ico" /> -->
     <link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text x=%2250%%22 y=%2250%%22 style=%22dominant-baseline:central;text-anchor:middle;font-size:110px;%22>🍒</text></svg>" />
     <meta name="viewport" content="width=device-width, initial-scale=1,minimum-scale=1.0, maximum-scale=1.0, user-scalable=no,minimal-ui,viewport-fit=cover" />
-    <title>24680.com - Casino & Sports & Racing</title>
+    <title>24680.com - Slots Online & Jogos com Dinheiro Real</title>
     <link rel="preconnect" href="https://fonts.googleapis.com">
     <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
     <link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@300;400;500;600;700;800&family=Open+Sans:wght@400;600;700&display=swap" rel="stylesheet">
-      <meta name="description"
-            content="Sequência Par: Dobre Sua Sorte no 24680.com! Melhor Cassino. Maior jogo crash. Comprovadamente justo &amp; Dealer ao vivo. 10000 jogos de slot. Bônus únicos &amp; rodadas grátis da sorte. Saques rápidos e Cofre lucrativo.">
-      <meta name="robots" content="NOODP">
-      <meta name="theme-color" content="#24262B">
-      <meta property="og:type" content="product">
-      <meta property="og:title" content="24680.com - Cassino & Esportes & Corridas">
-      <meta property="og:description" name="description"
-            content="Sequência Par: Dobre Sua Sorte no 24680.com! Melhor Cassino. Maior jogo crash. Comprovadamente justo &amp; Dealer ao vivo. 10000 jogos de slot. Bônus únicos &amp; rodadas grátis da sorte. BTC, ETH, DOGE, TRX, XRP, UNI, tokens defi suportados, saques rápidos e Cofre lucrativo.">
-      <meta property="og:image" content="https://24680.imgix.net/24680/web/header.jpg?auto=format,compress&cs=srgb">
-      <meta property="og:image:secure_url"
-            content="https://24680.imgix.net/24680/web/header.jpg?auto=format,compress&cs=srgb">
-      <meta property="og:site_name" content="24680.com">
-      <meta property="og:url" content="https://24680.com">
-      <meta name="twitter:card" content="summary_large_image">
-      <meta name="twitter:title" content="24680.com - Cassino & Esportes & Corridas">
-      <meta name="twitter:description"
-            content="Sequência Par: Dobre Sua Sorte no 24680.com! Melhor Cassino. Maior jogo crash. Comprovadamente justo &amp; Dealer ao vivo. 10000 jogos de slot. Bônus únicos &amp; rodadas grátis da sorte. BTC, ETH, DOGE, TRX, XRP, UNI, tokens defi suportados, saques rápidos e Cofre lucrativo.">
-      <meta name="twitter:image" content="https://24680.imgix.net/24680/web/header.jpg?auto=format,compress&cs=srgb">
-      <meta name="twitter:site" content="website">
-      <meta name="twitter:url" content="https://24680.com">
+
+    <meta name="description" content="%%BRAND_DESCRIPTION%%" />
+
+    <meta name="theme-color" content="%%THEME_COLOR%%" />
+
+    <meta property="og:type" content="website">
+    <meta property="og:site_name" content="%%BRAND_NAME%%">
+    <meta property="og:title" content="%%BRAND_OG_TITLE%%">
+    <meta property="og:description" content="%%BRAND_OG_DESCRIPTION%%">
+    <meta property="og:image" content="%%BRAND_OG_IMAGE%%">
+    <meta property="og:url" content="%%BRAND_URL%%">
+
+    <meta name="twitter:card" content="summary_large_image">
+    <meta name="twitter:title" content="%%BRAND_OG_TITLE%%">
+    <meta name="twitter:description" content="%%BRAND_OG_DESCRIPTION%%">
+    <meta name="twitter:image" content="%%BRAND_OG_IMAGE%%">
+    <meta name="twitter:url" content="%%BRAND_URL%%">
 
     <meta name="mobile-web-app-capable" content="yes">
     <meta name="apple-mobile-web-app-capable" content="yes">

+ 6 - 6
public/js/load.m.js

@@ -632,9 +632,9 @@
       "locale": "pt-BR",
       "languages": ["pt-BR", "en-US"],
       "defaultLang": "pt-BR",
-      "api": "https://brapi.vipsvip.org/",
-      "apiHost": "brapi.vipsvip.org",
-      "ws": "wss://brapi.vipsvip.org:10000",
+      "api": "https://brapi24680.vipsvip.org/",
+      "apiHost": "brapi24680.vipsvip.org",
+      "ws": "wss://brapi24680.vipsvip.org:10000",
       "wsPort": 10000,
       "phoneCode": "+55",
       "currency": "BRL",
@@ -642,7 +642,7 @@
       "timezone": "America/Sao_Paulo",
       "dateFormat": "DD/MM/YYYY",
       "numberFormat": "pt-BR",
-      "supportEmailDomain": "brg77.com",
+      "supportEmailDomain": "24680.com",
       "titleSuffix": "Slots Online & Jogos com Dinheiro Real"
     },
     // 墨西哥区域(预留,业务 APP_COUNTRY_CODE.MX)
@@ -715,7 +715,7 @@
     // ========== 巴西品牌站点配置 (pt-BR) ==========
     // BRG77 - 巴西默认主站(金币金 + 巴西绿 + 火焰红)
     "br24680": {
-      "title": "24680.com",
+      "title": "24680.com Cassino tradicional com dinheiro real",
       "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",
@@ -723,7 +723,7 @@
       "icon": `/${res_path}/icons/24680.webp`,
       "favicon": `data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text x=%2250%%22 y=%2250%%22 style=%22dominant-baseline:central;text-anchor:middle;font-size:110px;%22>🍒</text></svg>`,
       "region": "br",
-      "theme": defaultTheme
+      "theme": ouroEoGolTheme
     },
 
   }

+ 30 - 7
src/components/AutoWinList/index.jsx

@@ -18,6 +18,7 @@ import {numFormatter} from "@utils/helpers";
 import LazyImgix from "@components/Imgix/LazyImgix";
 import {useSelector} from "react-redux";
 import {getOrderedGames} from "@features/pageSlice";
+import {getConfig} from "@features/paySlice";
 
 
 const i18n=global.i18n;
@@ -56,6 +57,25 @@ const pickWeighted=(values,weights)=>{
     }
     return values[values.length-1];
 };
+// Build deposit-amount picker from the actual recharge config (favorable_price/money tiers).
+// Earlier tiers (cheaper packages) get a higher weight so the marquee feels realistic.
+const buildDepositPicker=(config)=>{
+    let tiers=null;
+    if (Array.isArray(config)){
+        const arr=config
+            .map(it=>Number(it?.favorable_price ?? it?.money ?? 0))
+            .filter(v=>v>0);
+        if (arr.length>0){
+            // de-dup + sort ascending
+            tiers=Array.from(new Set(arr)).sort((a,b)=>a-b);
+        }
+    }
+    if (!tiers||tiers.length===0) tiers=DEPOSIT_TIERS;
+    // weight: cheapest tier slightly less than the next two; long tail tapers off
+    const baseWeights=[3,14,20,18,14,11,9,7,4,3,2,1,1,1,1];
+    const weights=tiers.map((_,i)=>baseWeights[Math.min(i,baseWeights.length-1)]);
+    return ()=>pickWeighted(tiers,weights);
+};
 const pickDepositAmount=()=>pickWeighted(DEPOSIT_TIERS,DEPOSIT_WEIGHTS);
 // Withdrawals never show amounts under 100. Snap to a "round" amount in the
 // payout-sized range to feel realistic.
@@ -67,14 +87,15 @@ const pickWithdrawAmount=(seed)=>{
     return pickWeighted(WITHDRAW_TIERS,WITHDRAW_WEIGHTS);
 };
 
-const buildQueue=(list,games,brand)=>{
+const buildQueue=(list,games,brand,depositPicker)=>{
     const wins=[]; const txns=[];
+    const pickDeposit=typeof depositPicker==='function'?depositPicker:pickDepositAmount;
     list.forEach(item=>{
         const r=Math.random();
         const amount=Number(item.win)||0;
         if (r<0.40){
             const kind=Math.random()<0.5?'withdraw':'deposit';
-            const txAmount=kind==='deposit'?pickDepositAmount():pickWithdrawAmount(amount);
+            const txAmount=kind==='deposit'?pickDeposit():pickWithdrawAmount(amount);
             txns.push({nick:item.nick,amount:txAmount,kind});
         } else {
             const game=pickWeightedGame(games);
@@ -121,7 +142,7 @@ const Entry=({item,idx,phase,formatGolds,onClickGame,wrapClass})=>{
         to=setTimeout(()=>{ raf=requestAnimationFrame(tick); },startDelay);
         return ()=>{ if (to) clearTimeout(to); if (raf) cancelAnimationFrame(raf); };
     },[idx,phase,win]);
-    const middleText=item.type==='win'?item.gameName:(item.type==='withdraw'?'Withdraw':'Deposit');
+    const middleText=item.type==='win'?item.gameName:(item.type==='withdraw'?i18n.t('win_list.withdraw'):i18n.t('win_list.deposit'));
     const middleClass=item.type==='win'?styles.gameName:(item.type==='withdraw'?styles.actionWithdraw:styles.actionDeposit);
     const amountTone=item.type==='withdraw'?styles.amountWithdraw:(item.type==='deposit'?styles.amountDeposit:'');
     const amountClass=`${styles.amount} ${popped?styles.pop:''} ${amountTone}`;
@@ -168,7 +189,7 @@ const VRotator=({queue,formatGolds,onClickGame})=>{
             <div className={wrapClass}>
                 <div className={styles.marqueeWrap}>
                     <div key={`mq${idx}`} className={styles.marqueeText}>
-                        🎉&nbsp;&nbsp;Welcome to <b>{item.brand}</b>&nbsp;&nbsp;·&nbsp;&nbsp;Good luck &amp; big wins!&nbsp;&nbsp;🎰
+                        🎉&nbsp;&nbsp;{i18n.t('win_list.welcome')} <b>{item.brand}</b>&nbsp;&nbsp;·&nbsp;&nbsp;{i18n.t('win_list.good_luck')}&nbsp;&nbsp;🎰
                     </div>
                 </div>
             </div>
@@ -184,7 +205,7 @@ const VRotator=({queue,formatGolds,onClickGame})=>{
                             <span key={i} className={styles.txnLine}>
                                 <span className={styles.txnNick}>{t.nick}</span>
                                 <span className={t.kind==='withdraw'?styles.txnActWithdraw:styles.txnActDeposit}>
-                                    {' '}{t.kind==='withdraw'?'Withdrew':'Deposited'}{' '}
+                                    {' '}{t.kind==='withdraw'?i18n.t('win_list.withdrew'):i18n.t('win_list.deposited')}{' '}
                                 </span>
                                 <span className={t.kind==='withdraw'?styles.txnAmtWithdraw:styles.txnAmtDeposit}>
                                     {formatGolds(t.amount)}
@@ -207,6 +228,7 @@ const AutoWinList=({data})=>{
     const navigate=useNavigate();
     const lvicon=[lv_1,lv_2,lv_3];
     const orderedGames=useSelector(getOrderedGames);
+    const rechargeConfig=useSelector(getConfig);
     if (data==null)data={};
     // data={
     //     title:"win_list.big_wins",
@@ -452,11 +474,12 @@ const AutoWinList=({data})=>{
             swiper.update();
         }
     }, [swiper]);
+    const depositPicker=React.useMemo(()=>buildDepositPicker(rechargeConfig),[rechargeConfig]);
     const queue=React.useMemo(()=>{
         const list=data?.data||[];
         if (list.length===0) return [];
-        return buildQueue(list,orderedGames,getSiteBrand());
-    },[data,orderedGames]);
+        return buildQueue(list,orderedGames,getSiteBrand(),depositPicker);
+    },[data,orderedGames,depositPicker]);
     const renderVAuto=()=>{
         return (
             <div className={styles.rotator_container}>

+ 3 - 1
src/components/GameList/index.jsx

@@ -196,7 +196,9 @@ const GameList=({data,status,reqMore,user})=>{
                             let gridNum=item.gridNum!=null?item.gridNum:1;
                             // gridNum=Math.floor(Math.random()*3)+1;
                             let whStyle=gridInfo[gridNum];
-                            if (whStyle==null)return (<></>);
+                            // map 内的 fallback 必须返回带 key 的节点,否则触发
+                            // "Each child in a list should have a unique key" 警告。
+                            if (whStyle==null)return null;
                             return (
                                 <GameItem key={index} data={item} handleRoom={handlePopSelected} whStyle={whStyle}></GameItem>
                             )

+ 3 - 1
src/components/GameSmallList/index.jsx

@@ -311,7 +311,9 @@ const GameSmallList=({data,user})=>{
                             let gridNum=item.gridNum!=null?item.gridNum:1;
                             // gridNum=Math.floor(Math.random()*3)+1;
                             let whStyle=gridInfo[gridNum];
-                            if (whStyle==null)return (<></>);
+                            // map fallback 必须返回带 key 的节点,否则触发
+                            // "Each child in a list should have a unique key" 警告。
+                            if (whStyle==null)return null;
                             return (
                                 <GameItem whStyle={whStyle} key={index} data={item} handleRoom={handlePopSelected}></GameItem>
                             )

+ 44 - 1
src/components/UInput/PhoneInput.jsx

@@ -8,6 +8,7 @@ import RadioBtn from "@components/RadioBtn";
 import MuiDrawer from "@mui/material/Drawer";
 import {localStorageGet, localStorageSet} from "@utils/helpers";
 import {LOCAL_DEFAULT_COUNTRY} from "@constants/localkey";
+import {applyPhoneMask, getPhoneFormat, stripDigits} from "@db/phoneFormats";
 const i18n=global.i18n;
 const getDefaultAreaCode=(area)=>{
     let code="";
@@ -34,6 +35,44 @@ const PhoneInput = ({innerRef, ...props}) => {
     const wrapClassName=props.className;
     const message=props.message?props.message:"";
     props.className=props.inputClass?props.inputClass:"";
+    // ---- per-country phone formatting ---------------------------------
+    // Parent passes raw digits via `value` and receives raw digits via
+    // onChange (e.target.value). This component handles masked display +
+    // digits-only input, driven by the active country's format config.
+    const phoneFmt = getPhoneFormat(country);
+    const rawDigits = stripDigits(props.value);
+    const maskedDisplay = applyPhoneMask(rawDigits, phoneFmt.mask);
+    const userOnChange = props.onChange;
+    const phoneOnChange = (e) => {
+        if (!userOnChange) return;
+        let digits = stripDigits(e.target.value);
+        if (phoneFmt.maxDigits && digits.length > phoneFmt.maxDigits) {
+            digits = digits.substring(0, phoneFmt.maxDigits);
+        }
+        // synthesize an event-like object exposing the raw digits so all
+        // downstream form state stores pure digits (matches API expectations).
+        userOnChange({...e, target: {...e.target, value: digits}});
+    };
+    // Compose the placeholder: keep the caller's localized hint AND show the
+    // expected number format from the country profile so users see both.
+    // e.g. "Número de Telefone  (11) 91234-5678"
+    const placeholder = (() => {
+        const hint = props.placeholder || "";
+        const sample = phoneFmt.placeholderSample || "";
+        if (hint && sample) return `${hint}  ${sample}`;
+        return hint || sample;
+    })();
+    // strip the props we are about to override on the inner <input>, plus
+    // wrapper-only props that should never reach the native DOM element.
+    const {
+        value: _v,
+        onChange: _oc,
+        placeholder: _ph,
+        message: _m,
+        inputClass: _ic,
+        onChangeCountry: _occ,
+        ...restProps
+    } = props;
     const [showCountrySelect, setShowCountrySelect] = useState(false);
     const {isMobile} = useThemeProvider();
     const selectRef = useRef(null);
@@ -188,8 +227,12 @@ const PhoneInput = ({innerRef, ...props}) => {
                         <div className={styles.line}></div>
                         <input
                             type="tel"
+                            inputMode="numeric"
                             ref={innerRef}
-                            {...props}
+                            {...restProps}
+                            value={maskedDisplay}
+                            placeholder={placeholder}
+                            onChange={phoneOnChange}
                         />
                     </div>
                 </div>

+ 27 - 2
src/constants/virtualUser.js

@@ -1,4 +1,29 @@
-export const VirtualUsers = ["U***2326", "Fer*****satto", "U***3923", "Eli*****eoliv", "U***1773", "U***7097", "San*****silva", "Ros*****leide", "Rob*****veira", "U***1571", "U***8149", "Tha*****alita", "And*****erson", "U***1721", "U***1861", "U***2162", "Ste*****silva", "And*****rluis", "U***7323", "U***1696", "U***7788", "U***4175", "U***1433", "U***7029", "Lar*****rissa", "Cla*****audia", "Mar*****immed", "Dan*****dealc", "U***4466", "Gio*****beiro", "Edi*****silva", "Raq*****ramin", "Dan*****tista", "U***3521", "U***4329", "U***4462", "Ros*****adeso", "Wag*****chado", "Rob*****nlima", "U***8788", "U***2054", "Fla*****assis", "U***6774", "Sab*****santo", "U***3925", "U***4357", "U***8266", "U***6224", "U***3976", "Wes*****rbosa", "Cle*****silva", "Gin*****leysa", "Cri*****slope", "Rej*****silva", "U***5077", "U***5375", "U***2210", "U***8088", "Eli*****reira", "U***2459", "Flv*****stina", "U***5515", "U***7819", "Jor*****rente", "Jul*****ernan", "U***8744", "U***6511", "U***5689", "Car*****armen", "U***3916", "U***3095", "U***5608", "Ana*****luisa", "Val*****silva", "U***6759", "U***3878", "Nat*****rhara", "U***6442", "U***5024", "Nat*****halia", "Ped*****antos", "Fra*****elima", "Pat*****souza", "U***1554", "Ana*****chior", "Fel*****jesus", "Kat*****katia", "U***7037", "U***8136", "U***1203", "U***6630", "U***5771", "Ire*****lopes"]
+// 已脱敏的姓名样本,仅作为 ~10% 的少量出现,主体使用 UXXXX 数字账号。
+const MASKED_NAMES = [
+    "Fer*****satto","Eli*****eoliv","San*****silva","Ros*****leide","Rob*****veira",
+    "Tha*****alita","And*****erson","Ste*****silva","And*****rluis","Lar*****rissa",
+    "Cla*****audia","Mar*****immed","Dan*****dealc","Gio*****beiro","Edi*****silva",
+    "Raq*****ramin","Dan*****tista","Ros*****adeso","Wag*****chado","Rob*****nlima",
+    "Fla*****assis","Sab*****santo","Wes*****rbosa","Cle*****silva","Gin*****leysa",
+    "Cri*****slope","Rej*****silva","Eli*****reira","Flv*****stina","Jor*****rente",
+    "Jul*****ernan","Car*****armen","Ana*****luisa","Val*****silva","Nat*****rhara",
+    "Nat*****halia","Ped*****antos","Fra*****elima","Pat*****souza","Ana*****chior",
+    "Fel*****jesus","Kat*****katia","Ire*****lopes","Vit*****rocha"
+];
 
+// 历史导出(仍保留以兼容外部直接 import)
+export const VirtualUsers = MASKED_NAMES;
 
-export const getVirtualUsers = () => VirtualUsers;
+// UXXXX 4 位数字账号
+const makeUid = () => `U${Math.floor(1000 + Math.random() * 9000)}`;
+const pickMaskedName = () => MASKED_NAMES[Math.floor(Math.random() * MASKED_NAMES.length)];
+
+// 单条昵称:90% UXXXX,10% 脱敏姓名(与首页 AutoWinList 风格保持一致)
+const makeNick = () => (Math.random() < 0.1 ? pickMaskedName() : makeUid());
+
+// 给 marquee 用:每次返回一组随机昵称(默认 80 条),整体占比稳定 90/10
+export const getVirtualUsers = (count = 80) => {
+    const list = new Array(count);
+    for (let i = 0; i < count; i++) list[i] = makeNick();
+    return list;
+};

+ 110 - 0
src/db/phoneFormats.js

@@ -0,0 +1,110 @@
+// Phone-number format profiles keyed by area code (the country dial code).
+//
+// Each entry describes how to:
+//   - mask the input (digits-only typing, formatted for display)
+//   - validate (digit length / regex)
+//   - prompt the user (placeholder is an i18n key OR raw fallback string)
+//
+// To support a new region: add a new entry. The form will pick it up
+// automatically based on the selected country dial code.
+//
+// `mask`: chunk pattern. Each `#` is one digit slot, anything else is a
+// literal character that gets injected when there are enough digits to
+// reach it (e.g. "(##) #####-####").
+// `maxDigits`: hard cap on the digit count (mask length is used if omitted).
+// `validate`: optional fn(digits) => boolean — called after maxDigits trim.
+//             If omitted, "exactly maxDigits digits" is enforced.
+// `placeholderSample`: rendered example, shown verbatim as input placeholder.
+// `placeholderKey`: i18n key — preferred over `placeholderSample` when set.
+
+const validateBR = (d) => {
+    // 11 digits, area code 11..99, third digit must be 9
+    if (!d || d.length !== 11) return false;
+    const area = parseInt(d.substring(0, 2), 10);
+    const third = parseInt(d.charAt(2), 10);
+    return area >= 11 && area <= 99 && third === 9;
+};
+
+export const PHONE_FORMATS = {
+    // Brazil
+    "55": {
+        mask: "(##) #####-####",
+        maxDigits: 11,
+        validate: validateBR,
+        placeholderSample: "(11) 91234-5678",
+    },
+    // United States / Canada
+    "1": {
+        mask: "(###) ###-####",
+        maxDigits: 10,
+        placeholderSample: "(415) 555-0132",
+    },
+    // Mexico
+    "52": {
+        mask: "## #### ####",
+        maxDigits: 10,
+        placeholderSample: "55 1234 5678",
+    },
+    // China
+    "86": {
+        mask: "### #### ####",
+        maxDigits: 11,
+        placeholderSample: "138 0013 8000",
+    },
+    // United Kingdom
+    "44": {
+        mask: "#### ######",
+        maxDigits: 10,
+        placeholderSample: "7700 900123",
+    },
+};
+
+// A permissive default for area codes without an explicit profile.
+export const DEFAULT_PHONE_FORMAT = {
+    mask: null,           // no mask, but still digits-only
+    maxDigits: 15,        // E.164 upper bound
+    validate: (d) => !!d && d.length >= 6 && d.length <= 15,
+    placeholderSample: "",
+};
+
+export const getPhoneFormat = (country) => {
+    const key = String(country == null ? "" : country);
+    return PHONE_FORMATS[key] || DEFAULT_PHONE_FORMAT;
+};
+
+// Strip everything that isn't a digit.
+export const stripDigits = (s) => (s == null ? "" : String(s).replace(/\D+/g, ""));
+
+// Apply a mask string ("(##) #####-####") to a digit string.
+export const applyPhoneMask = (digits, mask) => {
+    if (!mask) return digits || "";
+    const out = [];
+    let di = 0;
+    for (let i = 0; i < mask.length && di < digits.length; i++) {
+        const ch = mask.charAt(i);
+        if (ch === "#") {
+            out.push(digits.charAt(di));
+            di++;
+        } else {
+            out.push(ch);
+        }
+    }
+    return out.join("");
+};
+
+export const formatPhoneByCountry = (country, raw) => {
+    const fmt = getPhoneFormat(country);
+    let digits = stripDigits(raw);
+    if (fmt.maxDigits && digits.length > fmt.maxDigits) {
+        digits = digits.substring(0, fmt.maxDigits);
+    }
+    return applyPhoneMask(digits, fmt.mask);
+};
+
+export const validatePhoneByCountry = (country, raw) => {
+    const fmt = getPhoneFormat(country);
+    const digits = stripDigits(raw);
+    if (typeof fmt.validate === "function") return fmt.validate(digits);
+    if (fmt.maxDigits) return digits.length === fmt.maxDigits;
+    return digits.length > 0;
+};

+ 29 - 0
src/db/routes.js

@@ -295,6 +295,35 @@ export const routes=[
         lpath:"#/signin",
         subs:[]
     },
+    // ↓↓↓ 桌面端用 hash 路由把 Promotion / Bonus 以 popup-big 弹窗形式呼出,
+    //    当前页面 URL/内容不变,仅在地址后追加 #/promotion 或 #/bonus。
+    //    type=1 (POP_OR_FULL):桌面=Popup、移动=SubScreen 全屏覆盖。
+    {
+        path: "#/promotion",
+        type: 1,
+        side: 0,
+        block:1,
+        title: "links.route_title.activity",
+        icon:"",
+        fill:";;",
+        component:"promotion",
+        login:0,
+        lpath:"#/signin",
+        subs:[]
+    },
+    {
+        path: "#/bonus",
+        type: 1,
+        side: 0,
+        block:1,
+        title: "links.route_title.activity",
+        icon:"",
+        fill:";;",
+        component:"bonus",
+        login:0,
+        lpath:"#/signin",
+        subs:[]
+    },
     {
         path: "#/firstdeposit",
         type: 1,

+ 17 - 0
src/features/mainSlice.js

@@ -151,8 +151,25 @@ const generateRoutes=(routes,asyncRoutes=[])=>{
             }
         });
     }
+    // 先走本地默认,保留侧栏顺序
     const allroutes=[...routes, ...asyncRoutes];
     _generate(allroutes,"","");
+    // 后端 asyncRoutes 覆盖本地路由的关键字段(login / lpath / type / state 等)
+    const _override=(list)=>{
+        Object.keys(list).map((key)=>{
+            const r=list[key];
+            if (!r) return;
+            const exist=allMap[r.path];
+            if (exist&&exist!==r){
+                if (r.login!=null) exist.login=r.login;
+                if (r.lpath!=null&&r.lpath!=="") exist.lpath=r.lpath;
+                if (r.type!=null) exist.type=r.type;
+                if (r.state!=null) exist.state=r.state;
+            }
+            if (r.subs!=null) _override(r.subs);
+        });
+    }
+    _override(asyncRoutes);
     const gnavs=generateNavs(allroutes);
     return {allMap,inRoutes,popRoutes,blockNavs:gnavs.navs,allNavs:gnavs.allNavs,allTopNavs:gnavs.allTopNavs};
 }

+ 1 - 1
src/features/paySlice.js

@@ -324,7 +324,7 @@ const checkPayMultiLeftTime=(data)=>{
             objInfo.cdTimer=setInterval(()=>{
                 timeleft--;
                 if (timeleft<=0){
-                    global.dispatch(reqFirstPayMulti());
+                    global.dispatch(reqFirstPayGift());
                     clearCDTimer();
                 }
                 else

+ 74 - 10
src/hooks/usePaymentActions.js

@@ -1,26 +1,91 @@
 import { ROUTE_PATH } from "@constants/routes";
-import { getFirstPayGift } from "@features/paySlice";
-import { isLogined } from "@features/mainSlice";
-import { useEffect, useRef } from "react";
-import { useSelector } from "react-redux";
+import { getCPFFirst, getFirstPayGift, getPaymentOrderStatus } from "@features/paySlice";
+import { getUserInfo, 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";
 
 const usePaymentActions = () => {
     const navigate = useNavigate();
+    const dispatch = useDispatch();
     const isLogin = useSelector(isLogined);
     const firstPayGift = useSelector(getFirstPayGift);
-    const firstPayGiftRef = useRef(firstPayGift);
-    useEffect(() => {
-        firstPayGiftRef.current = firstPayGift;
-    }, [firstPayGift]);
+    const user = useSelector(getUserInfo);
+    const cpfFirst = useSelector(getCPFFirst);
+    const orderStatus = useSelector(getPaymentOrderStatus);
+    const areaItem = global.getLocalAreaItem ? global.getLocalAreaItem() : null;
 
+    // 统一的下单入口:所有支付按钮(首充、每日礼包、节日活动、彩金等)都走这里。
+    // 之前这里只是 navigate(/pay),导致点击之后用户被甩回到充值页,根本没有
+    // 真正发起订单。现在补全 BR(CPF)/RU(支付方式)/EU(再次确认) 三种地区流程,
+    // 并在成功之后跳转到 PAY_ORDER。
     const startPayingOrder = (data = { price: 0, goodsName: "", payType: "", GiftsID: 0, gear: "" }) => {
         if (!isLogin) {
             navigate(ROUTE_PATH.SIGN_IN);
             return;
         }
+        if (orderStatus === REQ_STATUS.LOADING) return;
+
+        // 兼容老调用:保留 global.curGoods,方便其他地方读取当前下单参数
         global.curGoods = data;
-        navigate(ROUTE_PATH.PAY);
+
+        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();
+        }
     };
 
     const getFirstPayGiftInfo = () => {
@@ -41,7 +106,6 @@ const usePaymentActions = () => {
             received_bonus: 0,
             unlocked_bonus: 0,
         };
-        const firstPayGift = firstPayGiftRef.current;
         if (firstPayGift != null) {
             if (!isNaN(firstPayGift.time_end)) {
                 info.time_left = Math.floor((firstPayGift.time_end - Date.now()) / 1000);

+ 14 - 7
src/layout/Layout.jsx

@@ -41,6 +41,9 @@ import WAreaSelect from "@widgets/WAreaSelect";
 import WSaveAccount from "@widgets/WSaveAccount";
 import WBonusPack from "@widgets/WBonusPack";
 import WDepositInfo from "@widgets/WDeposit/WPayInfo/WDepositInfo";
+// 全局挂载 BR/RU 的下单前置弹窗,保证任何地方调用 startPayingOrder 都能弹出
+import BRCPFSave from "@widgets/WDeposit/BR/BRCPFSave/BRCPFSave";
+import RUPaytypeSelect from "@widgets/WDeposit/RU/RUPaytypeSelect";
 const PageNotFound = lazy(() => import('@pages/PageNotFound'));
 
 const Layout=({...props})=>{
@@ -100,14 +103,16 @@ const Layout=({...props})=>{
     }
     const renderInFull = () => {
         return (
-            <div className={`app fluid`} ref={appRef}>
-                <ScrollToTop/>
-                <div className="app_container_full">
-                    <Suspense fallback={<LoadingScreen/>}>
-                        <CBrowserRouter isLogin={isLogin} routes={inRoutes}/>
-                    </Suspense>
+            <>
+                <div className={`app fluid`} ref={appRef}>
+                    <ScrollToTop/>
+                    <div className="app_container_full">
+                        <Suspense fallback={<LoadingScreen/>}>
+                            <CBrowserRouter isLogin={isLogin} routes={inRoutes}/>
+                        </Suspense>
+                    </div>
                 </div>
-            </div>
+            </>
         )
     }
     const renderHash = () => {
@@ -140,6 +145,8 @@ const Layout=({...props})=>{
                 <WSaveAccount/>
                 <WBonusPack/>
                 <WDepositInfo/>
+                <BRCPFSave/>
+                <RUPaytypeSelect/>
             </>
         )
     }

+ 13 - 2
src/layout/Navbar/styles.module.scss

@@ -92,10 +92,16 @@
   height:2.75rem !important;
   margin-right: 0.5rem !important;
   font-weight: 600 !important;
+  white-space: nowrap !important;
+  text-transform: none !important;
   &_mobile{
     background-color: var(--nav-item-hover) !important;
-    width: 5.8rem !important;
+    width: auto !important;
+    min-width: 5.8rem !important;
     height:2rem !important;
+    padding: 0 0.75rem !important;
+    font-size: 0.85rem !important;
+    line-height: 1 !important;
   }
 }
 .btn_sign_up{
@@ -107,10 +113,15 @@
   background-image:conic-gradient(from 1turn,rgba(88,175,16,1),rgba(29,128,58,1));
   font-weight: 600 !important;
   margin-right: 1.5rem !important;
+  white-space: nowrap !important;
   &_mobile{
-    width: 5.8rem !important;
+    width: auto !important;
+    min-width: 5.8rem !important;
     height:2rem !important;
     margin-right: 0.1rem !important;
+    padding: 0 0.75rem !important;
+    font-size: 0.85rem !important;
+    line-height: 1 !important;
   }
 }
 .mobile-top-navigate-list{

+ 4 - 1
src/layout/Routes/index.jsx

@@ -18,12 +18,15 @@ const CreateRoute=(props)=>{
     const component=(route.component!=null&&route.component!=="")?route.component:"ModulePage";
     const Component = AllComponents[component];
     const topath=path?path:route.path;
+    // 把 key 从 props 中剥离,避免 spread 到 <RouteWrapper> 时触发
+    // React 18 的 "key prop is being spread into JSX" 警告。
+    const {key: _routeKey, ...restProps} = props;
     const checkLogin=()=>{
         if (!isLogin&&route.login===1){
             return (<Navigate to={route.lpath} replace={true}/>)
         }
         return (
-            <RouteWrapper {...{ ...props, Comp: Component, route: route }}  />
+            <RouteWrapper {...restProps} Comp={Component} route={route} />
         )
     }
     return (

+ 26 - 6
src/layout/Sidebar/index.jsx

@@ -156,7 +156,7 @@ const Sidebar = () => {
     const {pathname,hash} = useLocation();
     const navigate=useNavigate();
     const {width} = useWindowSize();
-    const {theme, toggleTheme} = useThemeProvider();
+    const {theme, toggleTheme, setIsShowLang, isMobile} = useThemeProvider();
 
     const [isActHovered, setIsActHovered] = useState(false);
 
@@ -346,9 +346,14 @@ const Sidebar = () => {
     const renderBonus=()=>{
         let styles=getolstyles(0);
         const ICON_SIZE={width:'1.5rem',height:'1.5rem',flex:'none'};
+        // 桌面端用 hash 路由(#/promotion / #/bonus),由 RouteContainer 渲染成
+        // popup-big 弹窗,当前页面 URL/内容不变;移动端继续走 /promotion / /bonus 全屏页。
+        const desktop = !isMobile();
+        const promotionPath = desktop ? "#/promotion" : ROUTE_PATH.PROMOTION;
+        const bonusPath     = desktop ? "#/bonus"     : ROUTE_PATH.BONUS;
         const items=[
-            {title:'links.promotion',     icon:'icon_Giftbox', path:ROUTE_PATH.PROMOTION, color:'#3bc117'},
-            {title:'links.bonus',         icon:'icon_Gift',    path:ROUTE_PATH.BONUS,     color:'#b891ff'},
+            {title:'links.promotion',     icon:'icon_Giftbox', path:promotionPath, color:'#3bc117'},
+            {title:'links.bonus',         icon:'icon_Gift',    path:bonusPath,     color:'#b891ff'},
             {title:'links.share_rewards', icon:null,           path:ROUTE_PATH.CASH_WHEEL, spin:true},
         ];
         const isActive=(p)=>pathname===p||(p && pathname.startsWith(p) && p!=='/');
@@ -403,6 +408,21 @@ const Sidebar = () => {
             </>
         )
     }
+    const renderLanguage=()=>{
+        return (
+            <StyledBlock open={open}>
+                <MTooltip disableHoverListener={open} title={i18n.t("quickbar.language")} arrow placement="right">
+                    <StyledButton open={open} className="h4" onClick={()=>{
+                        if (typeof setIsShowLang === "function") setIsShowLang(true);
+                        setOpen(false);
+                    }}>
+                        <SvgIcon className={"icon"} herf={"icon_Language"}/>
+                        {open?i18n.t("quickbar.language"):""}
+                    </StyledButton>
+                </MTooltip>
+            </StyledBlock>
+        )
+    }
     const renderSuport=()=>{
         return (
             <StyledBlock open={open}>
@@ -482,7 +502,7 @@ const Sidebar = () => {
             </StyledBlock>
         )
     }
-    const renderContent=()=>{
+    const renderContent=(showLanguage=false)=>{
         return (
             <>
                 {renderBonus()}
@@ -490,6 +510,7 @@ const Sidebar = () => {
                 {renderLine()}
                 {renderLinkBlocks()}
                 {renderSuport()}
+                {showLanguage?renderLanguage():null}
                 {renderThemeTabs()}
             </>
         )
@@ -548,14 +569,13 @@ const Sidebar = () => {
                 <div className={`d-flex flex-column sidebar-nav ${visiblecls}`}>
                     <div className={`d-flex flex-column g-4 ${visiblecls}`}>
                         {
-                            renderContent()
+                            renderContent(true)
                         }
                     </div>
                 </div>
             </MuiDrawer>
         )
     }
-    const {isMobile} = useThemeProvider();
     return (
         <>
             {

+ 23 - 2
src/locales/languages/en_US.json

@@ -1,4 +1,12 @@
 {
+  "quickbar": {
+    "first_deposit": "1st Deposit",
+    "share_earn": "Share & Earn",
+    "daily_bonus": "Daily Bonus",
+    "rescue": "Rescue",
+    "support": "Support",
+    "language": "Language"
+  },
   "links": {
     "home": "Home",
     "menu_title": {
@@ -104,7 +112,13 @@
   "win_list": {
     "big_wins": "Recent Big Wins",
     "tab_all": "All",
-    "tab_slots": "Slots"
+    "tab_slots": "Slots",
+    "welcome": "Welcome to",
+    "good_luck": "Good luck & big wins!",
+    "withdraw": "Withdraw",
+    "deposit": "Deposit",
+    "withdrew": "Withdrew",
+    "deposited": "Deposited"
   },
   "small_list": {
     "view_all": "View all"
@@ -168,7 +182,14 @@
     "last_login": "Last Used",
     "login_or": "or",
     "connect": "Connect",
-    "free_bonus_tips": "Free Bonus"
+    "free_bonus_tips": "Free Bonus",
+    "next": "Next",
+    "back": "Back",
+    "almost_there": "Almost there! Verify your number.",
+    "otp_sent_to": "Enter the 4-digit code sent by SMS to",
+    "change_phone": "Change",
+    "resend_in": "Resend soon…",
+    "resend_now": "Resend code"
   },
   "logout": {
     "success": "You have exited successfully."

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

@@ -1,4 +1,12 @@
 {
+  "quickbar": {
+    "first_deposit": "1er Depósito",
+    "share_earn": "Compartir y Ganar",
+    "daily_bonus": "Bono Diario",
+    "rescue": "Rescate",
+    "support": "Soporte",
+    "language": "Idioma"
+  },
   "links": {
     "home": "Inicio",
     "menu_title": {
@@ -104,7 +112,13 @@
   "win_list": {
     "big_wins": "Grandes Ganancias Recientes",
     "tab_all": "Todos",
-    "tab_slots": "Tragamonedas"
+    "tab_slots": "Tragamonedas",
+    "welcome": "Bienvenido a",
+    "good_luck": "¡Buena suerte y grandes premios!",
+    "withdraw": "Retiro",
+    "deposit": "Depósito",
+    "withdrew": "Retiró",
+    "deposited": "Depositó"
   },
   "small_list": {
     "view_all": "Ver todos"
@@ -168,7 +182,14 @@
     "connect": "Conectar",
     "free_bonus_tips": "Bono Gratis",
     "last_login": "Último Uso",
-    "login_or": "o"
+    "login_or": "o",
+    "next": "Siguiente",
+    "back": "Atrás",
+    "almost_there": "¡Ya casi! Verifica tu número.",
+    "otp_sent_to": "Ingresa el código de 4 dígitos enviado por SMS a",
+    "change_phone": "Cambiar",
+    "resend_in": "Reenviar pronto…",
+    "resend_now": "Reenviar código"
   },
   "logout": {
     "success": "Has salido exitosamente."

+ 29 - 8
src/locales/languages/pt_BR.json

@@ -1,4 +1,12 @@
 {
+  "quickbar": {
+    "first_deposit": "1º Depósito",
+    "share_earn": "Indicar & Ganhar",
+    "daily_bonus": "Bônus Diário",
+    "rescue": "Resgate",
+    "support": "Suporte",
+    "language": "Idioma"
+  },
   "links": {
     "home": "Início",
     "menu_title": {
@@ -86,8 +94,8 @@
     "dark": "Escuro",
     "light": "Claro"
   },
-  "sign_in": "Entrar",
-  "sign_up": "Registrar",
+  "sign_in": "Login",
+  "sign_up": "Cadastre-se",
   "footer": {
     "game_res": "Responsabilidades de Jogo",
     "privacy_policy": "Política de Privacidade",
@@ -104,7 +112,13 @@
   "win_list": {
     "big_wins": "Grandes Ganhos Recentes",
     "tab_all": "Todos",
-    "tab_slots": "Caça-níqueis"
+    "tab_slots": "Caça-níqueis",
+    "welcome": "Bem-vindo ao",
+    "good_luck": "Boa sorte & grandes prêmios!",
+    "withdraw": "Saque",
+    "deposit": "Depósito",
+    "withdrew": "Sacou",
+    "deposited": "Depositou"
   },
   "small_list": {
     "view_all": "Ver todos"
@@ -132,16 +146,16 @@
     "no_result": "Nenhum jogo encontrado"
   },
   "login": {
-    "login_title": "Entrar",
+    "login_title": "Login",
     "phone_ph": "Número de Telefone",
     "login_pwd": "Senha",
     "forgot": "Esqueceu sua senha?",
-    "sign_in_btn": "Entrar",
+    "sign_in_btn": "Login",
     "reg_new_des": "Novo no 24680?",
     "reg_new_btn": "Criar Conta",
-    "reg_title": "Registrar",
+    "reg_title": "Cadastre-se",
     "re_pwd": "Confirmar Senha",
-    "sign_up_btn": "Registrar",
+    "sign_up_btn": "Cadastre-se",
     "login_des": "Já tem uma conta?",
     "account": {
       "empty": "Por favor, insira o nome de usuário!"
@@ -172,7 +186,14 @@
     "connect": "Conectar",
     "free_bonus_tips": "Bônus Grátis",
     "last_login": "Último acesso",
-    "login_or": "ou"
+    "login_or": "ou",
+    "next": "Próximo",
+    "back": "Voltar",
+    "almost_there": "Quase lá! Verifique seu número.",
+    "otp_sent_to": "Insira o código de 4 dígitos enviado por SMS para",
+    "change_phone": "Alterar",
+    "resend_in": "Reenviar em breve…",
+    "resend_now": "Reenviar código"
   },
   "logout": {
     "success": "Você saiu com sucesso."

+ 8 - 5
src/utils/helpers.js

@@ -252,11 +252,14 @@ export const validateBRPhone=(t)=>{
 
 }
 export const validateCountryPhone=(country,phone)=>{
-    //只检查br
-    if (country==="55"){
-        return validateBRPhone(phone);
-    }
-    else{
+    // Delegate to per-country phone format config so new regions can be added
+    // by editing src/db/phoneFormats.js without touching this helper.
+    try {
+        // require lazily to avoid any circular-import risk
+        const { validatePhoneByCountry, stripDigits } = require("@db/phoneFormats");
+        return validatePhoneByCountry(country, stripDigits(phone));
+    } catch (e) {
+        if (country==="55") return validateBRPhone(phone);
         return true;
     }
 }

+ 6 - 6
src/widgets/HomeQuickBar/index.jsx

@@ -82,7 +82,7 @@ const HomeQuickBar = () => {
     const dailyGift = useSelector(getDailyGift);
 
     const firstBonusPct = firstPayGift?.gift_info?.total_bonus
-        ? (firstPayGift.gift_info.total_bonus - 100)
+        ? firstPayGift.gift_info.total_bonus
         : 0;
     const noFirstPay = !!firstPayGift?.has_purchased;
     const showFirstRed = !noFirstPay && firstBonusPct > 0;
@@ -106,7 +106,7 @@ const HomeQuickBar = () => {
                 <use xlinkHref={`${symbol}#icon_Share`}/>
             </svg>
         ),
-        label: "Share & Earn",
+        label: i18n.t("quickbar.share_earn"),
         value: null,
         red: false,
         onClick: () => navigate(ROUTE_PATH.AGENT),
@@ -117,7 +117,7 @@ const HomeQuickBar = () => {
                 <use xlinkHref={`${symbol}#icon_Giftbox`}/>
             </svg>
         ),
-        label: "1st Deposit",
+        label: i18n.t("quickbar.first_deposit"),
         value: showFirstRed ? `+${firstBonusPct}%` : null,
         red: showFirstRed,
         onClick: () => {
@@ -131,7 +131,7 @@ const HomeQuickBar = () => {
         {
             key: "daily",
             iconNode: <DailyBonusIcon/>,
-            label: "Daily Bonus",
+            label: i18n.t("quickbar.daily_bonus"),
             value: null,
             red: dailyCanClaim,
             onClick: () => global.showDailyGift && global.showDailyGift(),
@@ -139,7 +139,7 @@ const HomeQuickBar = () => {
         {
             key: "relief",
             iconNode: <RescueIcon/>,
-            label: "Rescue",
+            label: i18n.t("quickbar.rescue"),
             value: reliefAmount > 0 ? `${cur}${formatCash(reliefAmount, 0)}` : null,
             red: false,
             onClick: () => global.showProtectReward && global.showProtectReward(),
@@ -151,7 +151,7 @@ const HomeQuickBar = () => {
                     <use xlinkHref={`${symbol}#icon_Support`}/>
                 </svg>
             ),
-            label: "Support",
+            label: i18n.t("quickbar.support"),
             value: null,
             red: false,
             onClick: () => {

+ 161 - 58
src/widgets/LoginForm/index.jsx

@@ -61,6 +61,88 @@ const objInfo={
     inSubmit:false
 }
 
+// Compact OTP entry: N individual single-digit boxes. First box is focused
+// on mount; typing a digit auto-advances to the next box, Backspace on an
+// empty box jumps back, and pasting a code spreads it across all boxes.
+const OtpBoxes = ({length = 4, value = "", onChange, error = false}) => {
+    const refs = React.useRef([]);
+    React.useEffect(() => {
+        // focus the first box once when the OTP step appears
+        if (refs.current[0]) refs.current[0].focus();
+        // eslint-disable-next-line
+    }, []);
+    const digits = String(value || "").replace(/\D+/g, "").slice(0, length).split("");
+    while (digits.length < length) digits.push("");
+    const update = (next) => {
+        const joined = next.join("").slice(0, length);
+        if (onChange) onChange(joined);
+    };
+    const onBoxChange = (i, raw) => {
+        const ch = String(raw || "").replace(/\D+/g, "");
+        if (ch.length === 0) {
+            const next = [...digits];
+            next[i] = "";
+            update(next);
+            return;
+        }
+        // accept paste / multi-char by spreading across remaining boxes
+        const chars = ch.split("");
+        const next = [...digits];
+        let pos = i;
+        for (let k = 0; k < chars.length && pos < length; k++, pos++) {
+            next[pos] = chars[k];
+        }
+        update(next);
+        const target = Math.min(pos, length - 1);
+        if (refs.current[target]) refs.current[target].focus();
+    };
+    const onKeyDown = (i, e) => {
+        if (e.key === "Backspace" && !digits[i] && i > 0) {
+            e.preventDefault();
+            const next = [...digits];
+            next[i - 1] = "";
+            update(next);
+            if (refs.current[i - 1]) refs.current[i - 1].focus();
+        } else if (e.key === "ArrowLeft" && i > 0) {
+            if (refs.current[i - 1]) refs.current[i - 1].focus();
+        } else if (e.key === "ArrowRight" && i < length - 1) {
+            if (refs.current[i + 1]) refs.current[i + 1].focus();
+        }
+    };
+    const boxStyle = {
+        width: '3.25rem',
+        height: '3.25rem',
+        textAlign: 'center',
+        fontSize: '1.5rem',
+        fontWeight: 700,
+        color: 'var(--header)',
+        background: 'var(--input-bg, rgba(255,255,255,0.04))',
+        border: `1px solid ${error ? '#ff5252' : 'var(--input-border, rgba(255,255,255,0.15))'}`,
+        borderRadius: '0.6rem',
+        outline: 'none',
+        caretColor: '#3bc117',
+    };
+    return (
+        <div style={{display:'flex',justifyContent:'center',gap:'0.75rem'}}>
+            {digits.map((d, i) => (
+                <input
+                    key={i}
+                    ref={(el) => { refs.current[i] = el; }}
+                    type="text"
+                    inputMode="numeric"
+                    autoComplete="one-time-code"
+                    maxLength={1}
+                    value={d}
+                    onChange={(e) => onBoxChange(i, e.target.value)}
+                    onKeyDown={(e) => onKeyDown(i, e)}
+                    onFocus={(e) => e.target.select()}
+                    style={boxStyle}
+                />
+            ))}
+        </div>
+    );
+};
+
 const LoginForm = ({close}) => {
     const {theme,isMobile} = useThemeProvider();
     const [showType,setShowType]=useState(TYPE_LOGIN);
@@ -73,7 +155,8 @@ const LoginForm = ({close}) => {
     const dispatch = useDispatch();
     const navigate=useNavigate();
     const [loginType,setLoginType]=useState(LOGIN_TYPE_PHONE);
-    const {register, handleSubmit, formState: {errors}, control} = useForm({
+    const {register, handleSubmit, formState: {errors, isValid}, control, trigger, getValues, watch} = useForm({
+        mode: 'onChange',
         defaultValues: {
             // country:'55',
             account: '',
@@ -94,6 +177,10 @@ const LoginForm = ({close}) => {
     const smsLoginStatus=useSelector(getSMSLoginStatus);
     const isAccountLogin=useSelector(getAccountLogin);
     const [regType,setRegType]=useState(REG_TYPE_PHONE);
+    // SMS-register two-step flow:
+    //   1 = collect phone + password (then click "Next" to send OTP)
+    //   2 = enter OTP code, then submit registration
+    const [smsRegStep,setSmsRegStep]=useState(1);
     const loginOpen=useSelector(getLoginOpen);
     const registerOpen=useSelector(getRegisterOpen);
     //登录类型列表
@@ -201,6 +288,11 @@ const LoginForm = ({close}) => {
             setRegType(list[0].type);
         }
     },[registerOpen])
+    // Reset SMS-register flow back to step 1 when leaving the SMS reg tab
+    // or navigating between login/register pages.
+    useEffect(()=>{
+        setSmsRegStep(1);
+    },[regType,showType])
     //effect_______
     const navigatRoute=(route)=>{
         navigate(route,{replace:true});
@@ -765,12 +857,34 @@ const LoginForm = ({close}) => {
             </form>
         )
     }
-    const renderRegSms=()=>{
+    // ---- SMS Register (two-step) -------------------------------------
+    // Step 1: collect phone + password, then click "Next" to send the OTP
+    //         and advance to step 2.
+    // Step 2: show the 4-digit OTP input + countdown; submit triggers the
+    //         original reqRegister with all fields (password is mirrored
+    //         to repassword since we no longer ask the user twice).
+    const onClickRegSmsNext = async () => {
+        const ok = await trigger(['phone', 'password']);
+        if (!ok) return;
+        // fire OTP and switch to step 2
+        const phone = getValues('phone');
+        if (phone == null || phone === '') return;
+        if (otpStatus !== REQ_STATUS.LOADING) {
+            dispatch(reqSendCode({phone, country}));
+        }
+        setSmsRegStep(2);
+    };
+    const formatPhoneDisplay = (digits) => {
+        try {
+            const {applyPhoneMask, getPhoneFormat} = require('@db/phoneFormats');
+            return applyPhoneMask(digits || '', getPhoneFormat(country).mask);
+        } catch (e) {
+            return digits || '';
+        }
+    };
+    const renderRegSmsStep1 = () => {
         return (
-            <form key={"reg_sms"} autoComplete="off" className={styles.l_form} onSubmit={handleSubmit((data) => {
-                data.type=APP_LOGIN_TYPE.SMS;
-                onSubmitReg(data);
-            })}>
+            <form key={"reg_sms_1"} autoComplete="off" className={styles.l_form} onSubmit={(e)=>{e.preventDefault(); onClickRegSmsNext();}}>
                 <Controller control={control}
                             name="phone"
                             rules={{required: true, validate: validatePhone}}
@@ -801,63 +915,52 @@ const LoginForm = ({close}) => {
                                     innerRef={ref}/>
                             )}
                 />
-                <Controller control={control}
-                            name="repassword"
-                            rules={{required: true, minLength: 6}}
-                            render={({field: {ref, onChange, value}, fieldState: {error}}) => (
-                                <PasswordInput
-                                    className={classNames({'input-control--error': error})}
-                                    value={value}
-                                    message={error ? i18n.t("form_errors.pass") : ''}
-                                    onChange={e => onChange(e.target.value)}
-                                    placeholder={i18n.t('login.re_pwd')}
-                                    innerRef={ref}/>
-                            )}
-                />
+                {(() => {
+                    // gate the "Next" button on field validity (subscribe via watch)
+                    const phoneDigits = watch('phone') || '';
+                    const pwd = watch('password') || '';
+                    const phoneOk = validatePhone(phoneDigits);
+                    const pwdOk = pwd.length >= 6;
+                    const enabled = phoneOk && pwdOk && otpStatus !== REQ_STATUS.LOADING;
+                    return (
+                        <button type="submit" className={"button-green"} disabled={!enabled} style={!enabled?{opacity:0.5,cursor:'not-allowed'}:undefined}>
+                            {otpStatus === REQ_STATUS.LOADING ? renderLoading() : i18n.t("login.next")}
+                        </button>
+                    );
+                })()}
+                {renderRegToLogin()}
+            </form>
+        );
+    };
+    const renderRegSmsStep2 = () => {
+        return (
+            <form key={"reg_sms_2"} autoComplete="off" className={styles.l_form} onSubmit={handleSubmit((data) => {
+                data.type = APP_LOGIN_TYPE.SMS;
+                // user only entered password once — mirror to repassword for the API
+                data.repassword = data.password;
+                onSubmitReg(data);
+            })}>
                 <Controller control={control}
                             name="code"
-                            rules={{required: true}}
-                            render={({field: {ref, onChange, value}, fieldState: {error}}) => {
-                                return (
-                                    <div className={`d-flex flex-column g-6`}>
-                                        <div
-                                            className={`input-control ${classNames({'input-control--error': error})}`}>
-                                            <input
-                                                type={'text'}
-                                                autoComplete="off"
-                                                ref={ref}
-                                                onChange={e => onChange(e.target.value)}
-                                                placeholder={i18n.t('login.otp_ph')}
-                                            />
-                                            <button type={"button"}
-                                                    className={`${styles.otp_btn} ${otpCD > 0 ? styles.cd_timer : ''}`}
-                                                    onClick={onSendCode}>
-                                                {
-                                                    otpStatus === REQ_STATUS.LOADING ? (
-                                                        <CircularProgress color="success" size={22} thickness={5}/>) : (
-                                                        <>
-                                                            {
-                                                                otpCD <= 0 ? i18n.t('login.btn_otp') : otpCD + "S"
-                                                            }
-                                                        </>
-                                                    )
-                                                }
-                                            </button>
-                                        </div>
-                                    </div>
-                                )
-                            }}
+                            rules={{required: true, minLength: 4}}
+                            render={({field: {onChange, value}, fieldState: {error}}) => (
+                                <OtpBoxes length={4} value={value || ''} error={!!error} onChange={onChange}/>
+                            )}
                 />
-                <button className={"button-green"}>
-                    {
-                        regStatus === REQ_STATUS.LOADING ? renderLoading() : i18n.t("login.sign_up_btn")
-                    }
-                </button>
-                {/*{renderGuestReg()}*/}
+                {(() => {
+                    const code = watch('code') || '';
+                    const enabled = code.length >= 4 && regStatus !== REQ_STATUS.LOADING;
+                    return (
+                        <button type="submit" className={"button-green"} disabled={!enabled} style={!enabled?{opacity:0.5,cursor:'not-allowed'}:undefined}>
+                            {regStatus === REQ_STATUS.LOADING ? renderLoading() : i18n.t("login.sign_up_btn")}
+                        </button>
+                    );
+                })()}
                 {renderRegToLogin()}
             </form>
-        )
-    }
+        );
+    };
+    const renderRegSms = () => smsRegStep === 2 ? renderRegSmsStep2() : renderRegSmsStep1();
     const renderRegMail=()=>{
         return (
             <form key={"reg_mail"} autoComplete="off" className={styles.l_form} onSubmit={handleSubmit((data) => {

+ 8 - 9
src/widgets/WBonus/styles.module.scss

@@ -41,8 +41,6 @@
 
             .act_item {
                 margin-bottom: 0.73rem;
-                width: 100%;
-                height: 7.3rem;
                 position: relative;
 
                 display: flex;
@@ -50,18 +48,19 @@
                 align-items: flex-start;
                 justify-content: center;
 
-                width: 25.36rem;
-                height: 7.279rem;
+                // 让卡片始终撑满父容器宽度(桌面 popup-big 也能铺满),
+                // 用 aspect-ratio 维持原 25.36 : 7.279 ≈ 3.48 的比例。
+                width: 100%;
+                max-width: 40.6rem; // 桌面给一个上限(≈ 25.36rem * 1.6),避免极宽屏拉太大
+                aspect-ratio: 25.36 / 7.279;
+                height: auto;
 
                 .banner {
-                    width: 100%;
-                    height: 7.3rem;
                     position: absolute;
                     top: 0;
                     left: 0;
-
-                    width: 25.36rem;
-                    height: 7.279rem;
+                    width: 100%;
+                    height: 100%;
                 }
 
                 .title {

+ 73 - 54
src/widgets/WDeposit/BR/BRDeposit.jsx

@@ -11,7 +11,7 @@ import {
 } from "@features/paySlice";
 import fpayimg from "@assets/pay/fpayimg.png";
 import img_sc from "@assets/pay/img_sc_66.png";
-import React, {useState} from "react";
+import React, {useEffect, useMemo, useState} from "react";
 import Spring from "@components/Spring";
 import {REQ_STATUS} from "@api/config";
 import SmallLoading from "@components/SmallLoading";
@@ -51,6 +51,22 @@ const BRDeposit=()=>{
     const config=useSelector(getConfig);
     const orderStatus=useSelector(getPaymentOrderStatus);
     const [selectedID,setSelectedID]=useState("0");
+    // 推荐档位 ID:取后端 recommend==1 的档位;没有则取列表中位项做兜底。
+    // 列表头部的"🔥 Mais escolhido"提示与列表中那枚 🔥 标志都跟它走,
+    // 与用户当前点选的档位(active 绿框)无关。
+    const recommendID = useMemo(() => {
+        if (!Array.isArray(config) || config.length === 0) return "";
+        const r = config.find(g => g && g.recommend == 1) || config[Math.floor(config.length / 2)];
+        return r ? r.id : "";
+    }, [config]);
+    // 配置加载后,如果用户尚未手动选过档位,把默认选中初始化为推荐档位。
+    useEffect(() => {
+        if (!Array.isArray(config) || config.length === 0) return;
+        if (selectedID === "0" || selectedID === "" || !config.find(c => c.id === selectedID)){
+            const def = config.find(c => c && c.recommend == 1) || config[Math.floor(config.length / 2)];
+            if (def) setSelectedID(def.id);
+        }
+    }, [config]);
     const isLogin=useSelector(isLogined);
     const [selectedPromotion,setSelectedPromotion]=useState(false);
     const showBonus=useSelector(getShowBonus);
@@ -179,7 +195,8 @@ const BRDeposit=()=>{
         // if (firstpay==null)return (<></>);
         if (item==null)return (<></>);
         const total_bonus=firstPayGiftInfo?.total_bonus||0;
-        const bonus_percent=Math.max(total_bonus-100,0);
+        // 与左侧 Bonus +X% 文案保持一致,统一显示 total_bonus
+        const bonus_percent=total_bonus;
         const list=firstPayGiftInfo?.list||[];
         const recommend=list.find(g=>g.recommend==1)||list[Math.floor(list.length/2)]||{};
         const tier_price=Number(recommend.favorable_price||recommend.money||0);
@@ -201,22 +218,22 @@ const BRDeposit=()=>{
             }}>
                 <div className={styles.pay_info}>
                     <div className={styles.title}>{title}</div>
-                    <div className={`${styles.price_con} ${styles.price_con_offset}`}>
-                        <span className={styles.label}>{i18n.t("firstDeposit.bonus_label",{bonus:total_bonus})}</span>
-                    </div>
-                    {
-                        max_total>0 && (
-                            <div className={styles.price_con}>
-                                <span data-title={global.DOLLAR+trimZeroDecimals(max_total.toFixed(2))}
-                                      className={styles.p_text_f}>
-                                    {`${global.DOLLAR}${trimZeroDecimals(max_total.toFixed(2))}`}
+                    {/* 钱数(R$xxx)不再展示,把 Bônus +X% 用原来金额位置的大号金色字体展示 */}
+                    {(() => {
+                        const bonusText = i18n.t("firstDeposit.bonus_label",{bonus:total_bonus});
+                        return (
+                            <div className={`${styles.price_con} ${styles.price_con_offset}`}>
+                                <span data-title={bonusText} className={styles.p_text_f}>
+                                    {bonusText}
                                 </span>
                             </div>
-                        )
-                    }
-                    <button className={styles.price_btn}>
+                        );
+                    })()}
+                    {/* 外层 .first_pay 已是 <button>,这里改成 <span>
+                        以避免 <button> 嵌套(HTML 语义无效,React 会告警) */}
+                    <span className={styles.price_btn} role="presentation">
                         {btnlabel}
-                    </button>
+                    </span>
                 </div>
                 <img className={styles.gift_img} src={getGiftImg()}/>
                 <div className={styles.percent}>
@@ -229,29 +246,24 @@ const BRDeposit=()=>{
         )
     }
     const renderMultiFirst=()=>{
-        if (firstPayMulti===null)return (<></>);
-        let {status,items}=getMultiInfo();
+        // 改用 firstPayGift 数据驱动首充广告
+        const total_bonus=firstPayGiftInfo?.total_bonus||0;
+        const time_left=firstPayGiftInfo?.time_left||0;
+        const has_purchased=!!firstPayGiftInfo?.has_purchased;
+        // 没有首充礼包数据则不显示
+        if (total_bonus<=0) return (<></>);
+        // 已购买且活动已过期则不显示
+        if (has_purchased && firstPayGiftInfo?.expired>0 && Date.now()/1000>firstPayGiftInfo.expired) return (<></>);
+        // status: 0=倒计时中, -1=普通去支付, 1=已购买可领奖
+        // 未登录不显示倒计时
+        let status=-1;
+        if (isLogin && !has_purchased && time_left>0) status=0;
+        else if (isLogin && has_purchased) status=1;
+        const item={timeleft:time_left};
         return (
-            <StyledSwiper onSwiper={setSwiper}
-                          slidesPerView={1}
-                          spaceBetween={15}
-                          loop
-                          autoplay={{delay: 5000, disableOnInteraction: false}}
-                          effect="fade"
-                          fadeEffect={{crossFade: true}}
-                          modules={[Autoplay]}
-                          speed={1000}
-            >
-                {
-                    items.map((item,index)=>{
-                        return (
-                            <SwiperSlide key={index}>
-                                {renderFirst(item,status)}
-                            </SwiperSlide>
-                        )
-                    })
-                }
-            </StyledSwiper>
+            <div style={{width:'100%'}}>
+                {renderFirst(item,status)}
+            </div>
         )
     }
     const renderTitle = () => {
@@ -324,7 +336,9 @@ const BRDeposit=()=>{
                                 <span className={styles.label_dollar}>{global.DOLLAR}</span>
                                 <span className={styles.label_money}>{formatCurrency(item.money,i18n.getCurrLangKey())}</span>
                                 {
-                                    selectedID === item.id ? (
+                                    /* 🔥 标志只挂在推荐档位上,跟着选中走会让用户误以为
+                                       自己选什么都是推荐——这里改成与 selectedID 无关。 */
+                                    recommendID === item.id ? (
                                         <span className={styles.fire}>{"\u{1F525}"}</span>
                                     ) : null
                                 }
@@ -346,17 +360,16 @@ const BRDeposit=()=>{
     }
     const checkOrdinary=(price,goodsName,payType,GiftsID)=>{
         let isInFirst=false;
-        if (firstPayMulti!==null){
-            let items=firstPayMulti.items;
-            if (items!=null){
-                for (let i = 0; i < items.length; i++) {
-                    let item=items[i];
-                    let fmoney=Number(item.money);
-                    price=Number(price);
-                    if (fmoney===price){
-                        isInFirst=true;
-                        break;
-                    }
+        // 已购买或活动失效则不再走首充弹窗
+        const has_purchased=!!firstPayGiftInfo?.has_purchased;
+        const time_left=firstPayGiftInfo?.time_left||0;
+        if (!has_purchased && time_left>0){
+            const list=firstPayGiftInfo?.list||[];
+            for (let i=0;i<list.length;i++){
+                const fmoney=Number(list[i].favorable_price||list[i].money);
+                if (fmoney===Number(price)){
+                    isInFirst=true;
+                    break;
                 }
             }
         }
@@ -386,13 +399,19 @@ const BRDeposit=()=>{
             <button className={styles.deposit_btn} onClick={()=>{
                 let payItem=getSelectedConfig();
                 if (payItem==null)return;
-                let gear={};
-                if (payItem.gear!=null){
-                    gear=JSON.parse(payItem.gear);
-                    gear=gear[0];
+                // gear 字段是 JSON 字符串数组,可能为空字符串/null/解析失败/空数组,
+                // 任何一种都不能让后面 gear.id 抛 TypeError。
+                let gear=null;
+                if (payItem.gear!=null&&payItem.gear!==""){
+                    try{
+                        const parsed=JSON.parse(payItem.gear);
+                        if (Array.isArray(parsed)&&parsed.length>0)gear=parsed[0];
+                    }catch(e){
+                        gear=null;
+                    }
                 }
                 let money=payItem.money;
-                let id=gear.id;
+                let id=gear?gear.id:undefined;
                 let gift_id=payItem.gift_id;
                 if (selectedPromotion){
                     let activity=payItem.activity;

+ 8 - 3
src/widgets/WDeposit/BR/styles.module.scss

@@ -23,13 +23,16 @@
   & .pay_info{
     padding: 1.2rem;
     position: relative;
+    z-index: 4;            // 保证标题/Bônus/按钮始终盖在钱包图前面
+    pointer-events: auto;
     & .title{
       color: #fff;
       font-size: 1.2rem;
       font-weight: 700;
+      float: left;
     }
     & .price_btn{
-      margin-top: 0.8rem;
+      margin-top: 1.8rem;
       border-radius: 3rem;
       font-size: 1.2rem;
       font-weight: 600;
@@ -39,9 +42,10 @@
       background: linear-gradient(0deg, #f17e0a 0%, #faea93 100%);
       border: 1px solid #f0c461;
       text-shadow: -1px -1px 0 #987233, 1px -1px 0 #987233, -1px 1px 0 #987233, ;
+      float: left;
     }
     & .price_con_offset{
-      margin-top: 1rem;
+      margin-top: 2rem;
     }
     & .price_con{
       //margin-top: 1rem;
@@ -137,11 +141,12 @@
   }
   & .gift_img{
     position: absolute;
-    z-index: 2;
+    z-index: 1;            // 压在文字/按钮下面
     top: 0;
     right: 0;
     width: auto;
     height: 100%;
+    pointer-events: none;  // 不拦截按钮点击
   }
   & .percent{
     display: flex;

+ 2 - 2
src/widgets/WDeposit/index.jsx

@@ -4,7 +4,7 @@ import {useDispatch, useSelector} from "react-redux";
 import {getConfig, getPayConfigStatus, reqPayConfig} from "@features/paySlice";
 import {useEffect} from "react";
 import {getUserInfo} from "@features/mainSlice";
-import {reqFirstPayConfig, reqFirstPayMulti, reqRechargeConfig} from "@features/api";
+import {reqFirstPayConfig, reqFirstPayGift, reqRechargeConfig} from "@features/api";
 import {REQ_STATUS} from "@api/config";
 import SmallLoading from "@components/SmallLoading";
 import WWalletNav from "@widgets/WWalletNav";
@@ -25,7 +25,7 @@ const WDeposit=({close})=>{
             dispatch(reqRechargeConfig());
         }
         // dispatch(reqFirstPayConfig(userInfo.UserID))
-        dispatch(reqFirstPayMulti())
+        dispatch(reqFirstPayGift())
     },[])
     const renderDeposit=()=>{
         let areaItem=global.getLocalAreaItem();

+ 10 - 2
src/widgets/WDepositMarquee/index.jsx

@@ -6,11 +6,19 @@ const NAMES=['Rob','Mar','Lui','Car','Ana','Pedr','Joa','Fer','Gabr','Bru','Ren'
 const SUFFIXES=['veira','silva','santos','rocha','souza','lima','dias','melo'];
 const AMOUNTS=[20,50,100,150,200,300,500,1000,2000,3000];
 
-const makeRow=()=>{
+// 与首页 AutoWinList 风格保持一致:90% 概率使用 UXXXX(4 位数字)账号,
+// 10% 概率才使用脱敏姓名。
+const makeUid=()=>`U${Math.floor(1000+Math.random()*9000)}`;
+const makeMaskedName=()=>{
     const name=NAMES[Math.floor(Math.random()*NAMES.length)];
     const suf=SUFFIXES[Math.floor(Math.random()*SUFFIXES.length)];
+    return `${name}*****${suf}`;
+};
+const makeNick=()=> Math.random()<0.1 ? makeMaskedName() : makeUid();
+
+const makeRow=()=>{
     const amt=AMOUNTS[Math.floor(Math.random()*AMOUNTS.length)];
-    return {nick:`${name}*****${suf}`,amount:amt};
+    return {nick:makeNick(),amount:amt};
 };
 
 const WDepositMarquee=()=>{

+ 12 - 3
src/widgets/WFirstDeposit/index.jsx

@@ -134,6 +134,14 @@ const WFirstDeposit = () => {
     const sign_bonus_total = Math.floor(sign_bonus.reduce((sum, item) => sum + item, 0));
     const betting_total_bonus = betting_bonus.total_bonus + betting_task.total_bonus;
 
+    // 根据 has_purchased 的状态在 effect 阶段同步告知 SubScreen 是否清屏,
+    // 避免在渲染期 setState 触发 React 跨组件更新警告。
+    useEffect(() => {
+        if (typeof global.setSubScreenClear === "function") {
+            global.setSubScreenClear(!has_purchased);
+        }
+    }, [has_purchased]);
+
     useEffect(() => {
         // adjust 曝光充值页面
         if (global.tracker && global.tracker.trackEvent) {
@@ -237,8 +245,9 @@ const WFirstDeposit = () => {
     };
 
     const renderNotpurchase = () => {
-
-        global.setSubScreenClear && global.setSubScreenClear(true);
+        // setSubScreenClear 必须在 effect 阶段触发,否则会在 WFirstDeposit 渲染时
+        // 同步给父组件 SubScreen 派发 setState,触发 React 警告
+        // "Cannot update a component while rendering a different component"。
         return (
             <div className={styles.container} ref={rootRef}>
                 <div className={styles.top_box} ref={headerRef}>
@@ -434,7 +443,7 @@ const WFirstDeposit = () => {
 
 
     const rendePurchased = () => {
-        global.setSubScreenClear && global.setSubScreenClear(false);
+        // 见 renderNotpurchase 注释:移到下方 useEffect 里统一处理。
 
         var betting_need = giftInfo.betting_task.required_bet - giftInfo.betting_task.current_bet;
         if (betting_need < 0) betting_need = 0;

+ 37 - 29
src/widgets/WFirstDeposit/styles.module.scss

@@ -21,7 +21,10 @@ $bg: "./../../assets/firstPay/top_bg.png";
     // background-image: url($bg);
     // background-size: contain;
     background: url($bg) top / cover no-repeat;
-    background-color: #09251a;
+    // 顶部背景色:与主题深绿统一
+    background-color: #0e2a1a;
+    background-image: linear-gradient(135deg, #1d803ab3 0%, rgba(29, 128, 58, 1) 100%), url($bg);
+    background-blend-mode: multiply, normal;
     padding: var(--page-padding);
     padding-top: 3rem;
 
@@ -343,7 +346,8 @@ $bg: "./../../assets/firstPay/top_bg.png";
 .container2 {
     width: 100%;
     // height: 100%;
-    background-color: #0d1016;
+    // 主容器背景色:贴合应用深色主题
+    background-color: #0a1410;
     display: flex;
     justify-content: flex-start;
     align-items: center;
@@ -550,7 +554,7 @@ $bg: "./../../assets/firstPay/top_bg.png";
 
         .checkIn {
             width: 100%;
-            background-color: #232834;
+            background-color: #15281f;
 
             padding: 0.74rem;
             display: flex;
@@ -619,7 +623,8 @@ $bg: "./../../assets/firstPay/top_bg.png";
 
                     border-radius: 0.36765rem;
                     border: 1px solid rgba(255, 255, 255, 0.2);
-                    background: var(--bg03, #213743);
+                    // 单日签到卡:深绿表面
+                    background: var(--bg03, #122a1c);
 
                     margin: 0 auto;
                     position: relative;
@@ -668,13 +673,14 @@ $bg: "./../../assets/firstPay/top_bg.png";
                 }
                 .collecting {
                     border-radius: 0.36765rem;
-                    border: 1px solid #fef922;
+                    // 当前可领取:主题绿描边+辉光
+                    border: 1px solid #3bc117;
                     background: linear-gradient(
                             131deg,
-                            rgba(254, 249, 34, 0.2) 1.96%,
-                            rgba(254, 249, 34, 0) 95.04%
+                            rgba(59, 193, 23, 0.25) 1.96%,
+                            rgba(59, 193, 23, 0) 95.04%
                         ),
-                        #1b1f28;
+                        #0f1f17;
                 }
             }
 
@@ -682,7 +688,8 @@ $bg: "./../../assets/firstPay/top_bg.png";
                 width: 100%;
                 height: 2.72rem;
                 border-radius: 0.36765rem;
-                background-color: #1475e1;
+                // 默认(不可领)灰色;可领时由 .canClaim 覆盖为主题绿
+                background-color: #2a3a31;
 
                 color: #fff;
                 text-align: center;
@@ -691,18 +698,16 @@ $bg: "./../../assets/firstPay/top_bg.png";
                 font-style: normal;
                 font-weight: 600;
                 line-height: 2.72rem;
-
-                background-color: gray;
             }
             .canClaim {
-                background-color: #36ee85;
-                color: #13171f;
+                background: linear-gradient(135deg, #1d803a 0%, #3bc117 100%);
+                color: #0a1410;
             }
         }
 
         .betBox {
             width: 100%;
-            background-color: #232834;
+            background-color: #15281f;
 
             padding: 0.74rem;
             display: flex;
@@ -757,6 +762,7 @@ $bg: "./../../assets/firstPay/top_bg.png";
                     justify-content: center;
                     align-items: center;
                     border-radius: 7.35294rem;
+                    // VIP/标签底色:金色保留以做强调
                     background: #ffc700;
                     white-space: nowrap;
                 }
@@ -789,7 +795,7 @@ $bg: "./../../assets/firstPay/top_bg.png";
 
                     border-radius: 0.36765rem;
                     border: 1px solid rgba(255, 255, 255, 0.2);
-                    background: #1b1f28;
+                    background: #0f1f17;
 
                     .type {
                         color: #d5dceb;
@@ -831,7 +837,7 @@ $bg: "./../../assets/firstPay/top_bg.png";
                 gap: 0.74rem;
 
                 border-radius: 0.36765rem;
-                background: #1B1F28;
+                background: #0f1f17;
 
                 height: 4.044rem;
 
@@ -863,12 +869,14 @@ $bg: "./../../assets/firstPay/top_bg.png";
                 .progressBg {
                     width: 100%;
                     height: 0.59rem;
-                    background-color: #1a2c38;
+                    // 进度条底色:与主题深绿保持一致
+                    background-color: #0a1a12;
                     border-radius: 7.35294rem;
                     .progressBar {
                         height: 0.59rem;
                         border-radius: 7.35294rem;
-                        background: #fb6308;
+                        // 进度条主色:主题绿
+                        background: linear-gradient(90deg, #1d803a 0%, #3bc117 100%);
                     }
                 }
 
@@ -890,7 +898,7 @@ $bg: "./../../assets/firstPay/top_bg.png";
                         width: 4.7rem;
                         height: 1.69rem;
                         border-radius: 0.36765rem;
-                        background: gray;
+                        background: #2a3a31;
                         color: #fff;
                         text-align: center;
                         font-family: Inter;
@@ -901,8 +909,8 @@ $bg: "./../../assets/firstPay/top_bg.png";
                     }
 
                     .canConvert {
-                        background: #23FD93;
-                        color: #13171F;
+                        background: linear-gradient(135deg, #1d803a 0%, #3bc117 100%);
+                        color: #0a1410;
                     }
                 }
             }
@@ -910,7 +918,7 @@ $bg: "./../../assets/firstPay/top_bg.png";
 
         .turnoverBox {
             width: 100%;
-            background-color: #232834;
+            background-color: #15281f;
 
             padding: 0.74rem;
             display: flex;
@@ -963,19 +971,19 @@ $bg: "./../../assets/firstPay/top_bg.png";
                     justify-content: center;
                     align-items: center;
                     border-radius: 7.35294rem;
-                    background: #0CBA80;
+                    background: #3bc117;
                 }
             }
 
             .progressBg {
                 width: 100%;
                 height: 0.59rem;
-                background-color: #0D1016;
+                background-color: #0a1a12;
                 border-radius: 7.35294rem;
                 .progressBar {
                     height: 0.59rem;
                     border-radius: 7.35294rem;
-                    background: #0CBA80;
+                    background: linear-gradient(90deg, #1d803a 0%, #3bc117 100%);
                 }
             }
 
@@ -1013,7 +1021,7 @@ $bg: "./../../assets/firstPay/top_bg.png";
                 align-items: center;
 
                 border-radius: 0.36765rem;
-                background: #1B1F28;
+                background: #0f1f17;
 
                 .left {
                     display: flex;
@@ -1050,15 +1058,15 @@ $bg: "./../../assets/firstPay/top_bg.png";
                     line-height: 1.838rem;
 
                     border-radius: 0.36765rem;
-                    background: gray;
+                    background: #2a3a31;
 
                     width: 4.632rem;
                     height: 1.838rem;
                     text-align: center;
                 }
                 .canCollect {
-                    background: #23FD93;
-                    color: #13171F;
+                    background: linear-gradient(135deg, #1d803a 0%, #3bc117 100%);
+                    color: #0a1410;
                 }
             }
         }

+ 16 - 7
src/widgets/WRecharge/index.jsx

@@ -36,16 +36,25 @@ const WRecharge = ({ goods, onSelect, goodsName = "", giftId = 0, renderTitle =
     }
 
     const onPayment = () => {
-        // adjust 点击充值按钮
+        // adjust 点击充值按钮:tracker 在某些站点未注入(global.tracker 为空),
+        // 直接 .trackEvent 会抛 TypeError 阻断支付流程,统一加一层 safe 包裹。
+        const trackEvent = (name, payload) => {
+            try {
+                if (global.tracker && typeof global.tracker.trackEvent === "function") {
+                    if (payload !== undefined) global.tracker.trackEvent(name, payload);
+                    else global.tracker.trackEvent(name);
+                }
+            } catch (e) { /* tracker 故障不阻断支付 */ }
+        };
         if (global.enterRechargeType == 0) {
-            global.tracker.trackEvent("click_recharge", [{ key: "enterPayType", value: global.enterRechargeType }]);
-            global.tracker.trackEvent("click_recharge_count");
+            trackEvent("click_recharge", [{ key: "enterPayType", value: global.enterRechargeType }]);
+            trackEvent("click_recharge_count");
         } else if (global.enterRechargeType == 1) {
-            global.tracker.trackEvent("click_recharge_first_pay", [{ key: "enterPayType", value: global.enterRechargeType }]);
-            global.tracker.trackEvent("click_recharge_first_paycount");
+            trackEvent("click_recharge_first_pay", [{ key: "enterPayType", value: global.enterRechargeType }]);
+            trackEvent("click_recharge_first_paycount");
         } else if (global.enterRechargeType == 2) {
-            global.tracker.trackEvent("click_recharge_vip_hint", [{ key: "enterPayType", value: global.enterRechargeType }]);
-            global.tracker.trackEvent("click_recharge_vip_hint_count");
+            trackEvent("click_recharge_vip_hint", [{ key: "enterPayType", value: global.enterRechargeType }]);
+            trackEvent("click_recharge_vip_hint_count");
         }
 
         if (isFrequentTap("WRcharge_StartPayment")) return;

+ 3 - 3
src/widgets/WWelfarebar/index.jsx

@@ -1,6 +1,6 @@
 import styles from './styles.module.scss';
 import React, {useEffect, useRef, useState} from "react";
-import {reqFirstPayConfig, reqFirstPayMulti, reqGameInfo, reqProtectCheck, reqRechargeConfig} from "@features/api";
+import {reqFirstPayConfig, reqFirstPayGift, reqGameInfo, reqProtectCheck, reqRechargeConfig} from "@features/api";
 import {useDispatch, useSelector} from "react-redux";
 import {getFirstBonus, getOpenRelief, getPopBindPhone, getUpgradeBonus, getUserInfo} from "@features/mainSlice";
 import {APP_WELFARE} from "@constants/app";
@@ -142,7 +142,7 @@ const WWelfarebar = () => {
             if (orderInfo.status==="success"){
                 // dispatch(reqFirstPayConfig());
                 //pay_success
-                dispatch(reqFirstPayMulti(1));
+                dispatch(reqFirstPayGift());
             }
         }
     }
@@ -204,7 +204,7 @@ const WWelfarebar = () => {
         }
         dispatch(reqRechargeConfig());
         // dispatch(reqFirstPayConfig());
-        dispatch(reqFirstPayMulti());
+        dispatch(reqFirstPayGift());
     },[user.UserID]);
     useEffect(() => {
         checkProtectLimit();