浏览代码

no message

zhejiushizhuce 2 年之前
父节点
当前提交
3a1f31ab29
共有 58 个文件被更改,包括 1033 次插入 和 94 次删除
  1. 1 0
      package.json
  2. 2 0
      public/index.html
  3. 12 1
      src/App.js
  4. 3 0
      src/Global.js
  5. 8 0
      src/api/config.js
  6. 18 26
      src/api/request.js
  7. 3 1
      src/app/store.js
  8. 二进制
      src/assets/promotion/banner_1.png
  9. 10 0
      src/assets/promotion/icon_1.svg
  10. 4 0
      src/assets/singlesvg/share.svg
  11. 二进制
      src/assets/wheel/bg_light.png
  12. 二进制
      src/assets/wheel/cash_2.png
  13. 二进制
      src/assets/wheel/d.png
  14. 二进制
      src/assets/wheel/draw.png
  15. 二进制
      src/assets/wheel/light1.png
  16. 二进制
      src/assets/wheel/light2.png
  17. 二进制
      src/assets/wheel/num_0.png
  18. 二进制
      src/assets/wheel/num_1.png
  19. 二进制
      src/assets/wheel/pix_2.png
  20. 二进制
      src/assets/wheel/pointer.png
  21. 二进制
      src/assets/wheel/spin-1.png
  22. 二进制
      src/assets/wheel/spin-2.png
  23. 二进制
      src/assets/wheel/spin-4.png
  24. 二进制
      src/assets/wheel/spin-5.png
  25. 二进制
      src/assets/wheel/spinBg.png
  26. 1 1
      src/components/LoadingScreen.jsx
  27. 15 9
      src/components/UInput/PasswordInput.jsx
  28. 16 10
      src/components/UInput/PhoneInput.jsx
  29. 1 0
      src/constants/app.js
  30. 2 1
      src/constants/localkey.js
  31. 3 1
      src/constants/routes.js
  32. 8 1
      src/contexts/themeContext.js
  33. 26 0
      src/db/routes.js
  34. 35 3
      src/features/api.js
  35. 23 3
      src/features/mainSlice.js
  36. 36 0
      src/features/turnplateSlice.js
  37. 二进制
      src/fonts/swisscbt/SWISSC-BT.ttf
  38. 4 0
      src/fonts/swisscbt/style.css
  39. 2 2
      src/layout/BottomNav/index.jsx
  40. 8 0
      src/locales/languages/en_US.json
  41. 10 0
      src/pages/CashWheel.jsx
  42. 10 0
      src/pages/Promotion.jsx
  43. 5 1
      src/pages/index.jsx
  44. 2 1
      src/style.scss
  45. 3 0
      src/styles/_elements.scss
  46. 37 0
      src/styles/_keyframes.scss
  47. 3 0
      src/styles/_utils.scss
  48. 12 0
      src/utils/helpers.js
  49. 23 13
      src/widgets/LoginForm/index.jsx
  50. 288 0
      src/widgets/WCashWheel/WWheel/WWheel.jsx
  51. 148 0
      src/widgets/WCashWheel/WWheel/styles.module.scss
  52. 26 0
      src/widgets/WCashWheel/index.jsx
  53. 38 0
      src/widgets/WCashWheel/styles.module.scss
  54. 26 5
      src/widgets/WCustomerService/index.jsx
  55. 6 3
      src/widgets/WEditPassword/index.jsx
  56. 20 12
      src/widgets/WEditProfile/index.jsx
  57. 62 0
      src/widgets/WPromotion/index.jsx
  58. 73 0
      src/widgets/WPromotion/styles.module.scss

+ 1 - 0
package.json

@@ -34,6 +34,7 @@
     "country-state-city": "^3.1.2",
     "craco": "^0.0.3",
     "dayjs": "^1.11.7",
+    "fontfaceobserver": "^2.3.0",
     "gsap": "^3.12.5",
     "http-proxy-middleware": "^3.0.0",
     "i18next": "^23.8.2",

+ 2 - 0
public/index.html

@@ -31,6 +31,8 @@
     <meta name="x5-fullscreen" content="true">
     <meta name="360-fullscreen" content="true">
     <meta name="format-detection" content="telephone=no">
+    <link rel="manifest" href="https://progressier.app/AcUf7ja3Lb8f8w5NS4LT/progressier.json"/>
+    <script defer src="https://progressier.app/AcUf7ja3Lb8f8w5NS4LT/script.js"></script>
   </head>
   <body>
     <noscript>You need to enable JavaScript to run this app.</noscript>

+ 12 - 1
src/App.js

@@ -18,6 +18,7 @@ import 'swiper/css/pagination';
 
 // fonts
 import '@fonts/icomoon/icomoon.woff';
+// import '@fonts/swisscbt/SWISSC-BT.ttf'
 
 // contexts
 import {SidebarProvider, useSidebar} from '@contexts/sidebarContext';
@@ -43,12 +44,13 @@ import Layout from "@layout/Layout";
 import {getFetchStatus, getUserID, getUserInfo} from '@features/mainSlice';
 import {useDispatch, useSelector} from "react-redux";
 import {REQ_STATUS} from "@api/config";
-import {reqCustomService, reqGetRoutes, reqWithdrawBaseInfo} from "@features/api";
+import {reqCustomService, reqGetRoutes, reqTurnplateIndex, reqWithdrawBaseInfo} from "@features/api";
 import LoadingScreen from "@components/LoadingScreen";
 import {reqPayConfig} from "@features/paySlice";
 import {connectHall, disconnectHall} from "@features/actions/hallClient";
 import * as toast from "react-toastify";
 import {useBlocker} from "react-router";
+import FontFaceObserver from 'fontfaceobserver';
 const App = () => {
     const dispatch = useDispatch();
     const fetchStatus=useSelector(getFetchStatus);
@@ -73,8 +75,15 @@ const App = () => {
     const startReqRoutes=()=>{
         dispatch(reqGetRoutes());
     }
+    const loadFont=()=>{
+        let swisscbt=new FontFaceObserver("SWISSC-BT");
+        swisscbt.load().then(() => {
+
+        })
+    }
     useEffect(() => {
         // scroll to top on route change
+        loadFont();
         //请求routes
         startReqRoutes();
         appRef.current && appRef.current.scrollTo(0, 0);
@@ -94,6 +103,8 @@ const App = () => {
         reqPayConfig(dispatch,userInfo);
         //customer
         dispatch(reqCustomService());
+        //turnplate
+        dispatch(reqTurnplateIndex());
         // var client = new Colyseus.Client('wss://wstest.ouro777.com:10000');
         // console.log(client.joinOrCreate)
     },[userID])

+ 3 - 0
src/Global.js

@@ -15,3 +15,6 @@ global.msgBoxHide=()=>{
     if (global.msgBox)global.msgBox.hide();
 }
 global.authLockPath=null;
+global.device="mobile";
+global.width=100;
+global.height=100;

+ 8 - 0
src/api/config.js

@@ -47,3 +47,11 @@ export const APP_CHANGE_PAY_PASS=BASE_PREFIX+"changePayPass";
 export const APP_GAME_INFO=BASE_PREFIX+"info";
 //custom customer_service
 export const APP_CUSTOMER_SERVICE="api/api/customer_service";
+export const APP_CUSTOMER_LEAVEMSG="api/user/message";
+// /game/turnplate/index
+// /game/turnplate/kan
+// /game/turnplate/bonus
+//turnplate
+export const APP_TURNPLATE_INDEX=BASE_PREFIX+"turnplate/index";
+export const APP_TURNPLATE_KAN=BASE_PREFIX+"turnplate/kan";
+export const APP_TURNPLATE_BONUS=BASE_PREFIX+"turnplate/bonus";

+ 18 - 26
src/api/request.js

@@ -11,6 +11,16 @@ const getSign=()=>{
     const sign=localStorageGet(LOCAL_SIGN,"");
     return sign;
 }
+const getComParams=()=>{
+    let params={};
+    let sign=getSign();
+    if (sign!=null&&sign!="")params.sign=sign;
+    params.device=global.device;
+    params.width=global.width;
+    params.height=global.height;
+    params.t=Date.now();
+    return params;
+}
 const checkResponse=(data)=>{
     if (data){
         //登录失效
@@ -46,8 +56,6 @@ service.interceptors.request.use(
 // response pre-processing
 service.interceptors.response.use(
     response => {
-        // console.log(response);
-        // console.log(response.data)
         checkResponse(response.data);
         return response.data;
     },
@@ -61,52 +69,36 @@ service.interceptors.response.use(
 
 
 const get=(params={})=>{
-    let sign=getSign();
     if (params.params==null)params.params={};
-    if (sign!=null&&sign!=""){
-        // let url=params.url;
-        // if (url.indexOf("?")===-1)url=url+"?sign="+getSign();
-        // else url=url+"&sign="+getSign();
-        // params.url=url;
-        params.params["sign"]=sign;
-    }
-    params.params["t"]=Date.now();
+    let comParmas=getComParams();
+    params.params=Object.assign(params.params,comParmas);
     return service({
         method: 'get',
         ...params
     });
 }
 const post=(params={})=>{
-    let sign=getSign();
     if (params.data==null)params.data={};
-    if (sign!=null&&sign!=""){
-        params.data.sign=sign;
-    }
-    params.data.t=Date.now();
+    let comParmas=getComParams();
+    params.data=Object.assign(params.data,comParmas);
     return service({
         method: 'post',
         ...params
     });
 }
 const put=(params={})=>{
-    let sign=getSign();
     if (params.data==null)params.data={};
-    if (sign!=null&&sign!=""){
-        params.data.sign=sign;
-    }
-    params.data.t=Date.now();
+    let comParmas=getComParams();
+    params.data=Object.assign(params.data,comParmas);
     return service({
         method: 'put',
         ...params
     });
 }
 const adelete=(params={})=>{
-    let sign=getSign();
     if (params.data==null)params.data={};
-    if (sign!=null&&sign!=""){
-        params.data.sign=sign;
-    }
-    params.data.t=Date.now();
+    let comParmas=getComParams();
+    params.data=Object.assign(params.data,comParmas);
     return service({
         method: 'delete',
         ...params

+ 3 - 1
src/app/store.js

@@ -5,6 +5,7 @@ import PageReducer from '@features/pageSlice';
 import GameListReducer from '@features/gameListSlice';
 import PayReducer from '@features/paySlice';
 import SocketReducer from '@features/socketSlice';
+import TurnplateReducer from '@features/turnplateSlice';
 export default configureStore({
     reducer: {
         // todos: TodosReducer,
@@ -12,7 +13,8 @@ export default configureStore({
         page:PageReducer,
         gamelist:GameListReducer,
         pay:PayReducer,
-        socket:SocketReducer
+        socket:SocketReducer,
+        turnplate:TurnplateReducer
     },
     middleware:(getDefaultMiddleware => {
         let middlewares=getDefaultMiddleware();

二进制
src/assets/promotion/banner_1.png


文件差异内容过多而无法显示
+ 10 - 0
src/assets/promotion/icon_1.svg


+ 4 - 0
src/assets/singlesvg/share.svg

@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
+<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M13.803 5.33333C13.803 3.49238 15.3022 2 17.1515 2C19.0008 2 20.5 3.49238 20.5 5.33333C20.5 7.17428 19.0008 8.66667 17.1515 8.66667C16.2177 8.66667 15.3738 8.28596 14.7671 7.67347L10.1317 10.8295C10.1745 11.0425 10.197 11.2625 10.197 11.4872C10.197 11.9322 10.109 12.3576 9.94959 12.7464L15.0323 16.0858C15.6092 15.6161 16.3473 15.3333 17.1515 15.3333C19.0008 15.3333 20.5 16.8257 20.5 18.6667C20.5 20.5076 19.0008 22 17.1515 22C15.3022 22 13.803 20.5076 13.803 18.6667C13.803 18.1845 13.9062 17.7255 14.0917 17.3111L9.05007 13.9987C8.46196 14.5098 7.6916 14.8205 6.84848 14.8205C4.99917 14.8205 3.5 13.3281 3.5 11.4872C3.5 9.64623 4.99917 8.15385 6.84848 8.15385C7.9119 8.15385 8.85853 8.64725 9.47145 9.41518L13.9639 6.35642C13.8594 6.03359 13.803 5.6896 13.803 5.33333Z"/>
+</svg>

二进制
src/assets/wheel/bg_light.png


二进制
src/assets/wheel/cash_2.png


二进制
src/assets/wheel/d.png


二进制
src/assets/wheel/draw.png


二进制
src/assets/wheel/light1.png


二进制
src/assets/wheel/light2.png


二进制
src/assets/wheel/num_0.png


二进制
src/assets/wheel/num_1.png


二进制
src/assets/wheel/pix_2.png


二进制
src/assets/wheel/pointer.png


二进制
src/assets/wheel/spin-1.png


二进制
src/assets/wheel/spin-2.png


二进制
src/assets/wheel/spin-4.png


二进制
src/assets/wheel/spin-5.png


二进制
src/assets/wheel/spinBg.png


+ 1 - 1
src/components/LoadingScreen.jsx

@@ -13,4 +13,4 @@ const LoadingScreen = () => {
     )
 }
 
-export default LoadingScreen
+export default LoadingScreen

+ 15 - 9
src/components/UInput/PasswordInput.jsx

@@ -6,6 +6,7 @@ import SvgIcon from "@components/SvgIcon";
 const PasswordInput = ({innerRef, ...props}) => {
     const [isPasswordVisible, setIsPasswordVisible] = useState(false);
     const wrapClassName=props.className;
+    const message=props.message?props.message:"";
     props.className=props.inputClass?props.inputClass:"";
 
     const togglePasswordVisibility = e => {
@@ -18,15 +19,20 @@ const PasswordInput = ({innerRef, ...props}) => {
     }, [props.value]);
 
     return (
-        <div className={`input-control ${wrapClassName}`}>
-            <input
-                   type={isPasswordVisible ? 'text' : 'password'}
-                   autoComplete="off"
-                   ref={innerRef}
-                   {...props}/>
-            <button className={styles.eye_btn} onClick={togglePasswordVisibility} aria-label="Toggle password visibility">
-                <SvgIcon className={styles.icon} herf={`${isPasswordVisible?'icon_Hide':'icon_View'}`}></SvgIcon>
-            </button>
+        <div className={`d-flex flex-column g-6`}>
+            <div className={`input-control ${wrapClassName}`}>
+                <input
+                    type={isPasswordVisible ? 'text' : 'password'}
+                    autoComplete="off"
+                    ref={innerRef}
+                    {...props}/>
+                <button className={styles.eye_btn} onClick={togglePasswordVisibility} aria-label="Toggle password visibility">
+                    <SvgIcon className={styles.icon} herf={`${isPasswordVisible?'icon_Hide':'icon_View'}`}></SvgIcon>
+                </button>
+            </div>
+            {
+                message!==""?(<div className="input-prompt-error">{message}</div>):(<></>)
+            }
         </div>
     )
 }

+ 16 - 10
src/components/UInput/PhoneInput.jsx

@@ -4,23 +4,29 @@ import {useEffect, useState} from "react";
 const PhoneInput = ({innerRef, ...props}) => {
     const [country, setCountry] = useState("55");
     const wrapClassName=props.className;
+    const message=props.message?props.message:"";
     props.className=props.inputClass?props.inputClass:"";
     useEffect(() => {
         if (props.onChangeCountry!=null)props.onChangeCountry(country);
     }, [country]);
     return (
-        <div className={`input-control ${wrapClassName}`}>
-            <div className={styles.l_phone}>
-                <div className={styles.country}>
-                    <div>{"+"+country}</div>
+        <div className={`d-flex flex-column g-6`}>
+            <div className={`input-control ${wrapClassName}`}>
+                <div className={styles.l_phone}>
+                    <div className={styles.country}>
+                        <div>{"+"+country}</div>
+                    </div>
+                    <div className={styles.line}></div>
+                    <input
+                        type="tel"
+                        ref={innerRef}
+                        {...props}
+                    />
                 </div>
-                <div className={styles.line}></div>
-                <input
-                    type="tel"
-                    ref={innerRef}
-                    {...props}
-                />
             </div>
+            {
+                message!==""?(<div className="input-prompt-error">{message}</div>):(<></>)
+            }
         </div>
     )
 }

+ 1 - 0
src/constants/app.js

@@ -8,6 +8,7 @@ const APP_GENDER={
     OTHER:2,
 }
 const APP_CHANNEL={
+    PackageName:"com.ouro777.j15",
     Channel:"com.ouro777.j15",
     ChannelNumber:271
 }

+ 2 - 1
src/constants/localkey.js

@@ -3,4 +3,5 @@ const LOCAL_USER_INFO="USER_INFO";
 const LOCAL_IS_LOGIN="IS_LOGIN";
 const LOCAL_SIGN="G_SIGN";
 const LOCAL_GAME_URL="LOCAL_GAME_URL_";
-export {LOCAL_USER_INFO,LOCAL_IS_LOGIN,LOCAL_SIGN,LOCAL_GAME_URL};
+const LOCAL_CUSTOM_LOCK="LOCAL_CUSTOM_LOCK";
+export {LOCAL_USER_INFO,LOCAL_IS_LOGIN,LOCAL_SIGN,LOCAL_GAME_URL,LOCAL_CUSTOM_LOCK};

+ 3 - 1
src/constants/routes.js

@@ -22,7 +22,9 @@ const ROUTE_PATH={
     PAY_ORDER:"#/payorder",
     ACCOUNT_INFO:"#/accountInfo",
     CHANGE_PAY_PASS:"#/changePayPass",
-    CUSTOMER:"#/customer"
+    CUSTOMER:"#/customer",
+    PROMOTION:"#/promotion",
+    CASH_WHEEL:"#/cashwheel"
 }
 
 export {

+ 8 - 1
src/contexts/themeContext.js

@@ -9,7 +9,7 @@ import {getUserInfo, isLogined} from "@features/mainSlice";
 const ThemeContext = createContext(undefined);
 
 export const ThemeProvider = ({children}) => {
-    const {width} = useWindowSize();
+    const {width,height} = useWindowSize();
     const page = document.documentElement;
     const isRtl = isRtlLang(navigator.language);
     const browserTheme = window.matchMedia('(prefers-color-scheme: light)');
@@ -47,6 +47,13 @@ export const ThemeProvider = ({children}) => {
 
     const isLogin=useSelector(isLogined);
     const user=useSelector(getUserInfo);
+    const initWindowSize=()=>{
+        if (isMobile())global.device="mobile";
+        else global.device="pc";
+        global.width=width;
+        global.height=height;
+    }
+    initWindowSize();
     const stopTransition = () => {
         page.classList.add('no-transition');
         setTimeout(() => page.classList.remove('no-transition'), 100);

+ 26 - 0
src/db/routes.js

@@ -243,4 +243,30 @@ export const routes=[
         lpath:"#/signin",//登录path
         subs:[]
     },
+    {
+        path: "#/promotion",
+        type: 1,
+        side: 0,
+        block:1,//块排序
+        title: "All activities",
+        icon:"",
+        fill:";;",//icon 颜色 默认 选中 本身颜色
+        component:"promotion",
+        login:0,//0,1 检不检查登录
+        lpath:"#/signin",//登录path
+        subs:[]
+    },
+    {
+        path: "#/cashwheel",
+        type: 1,
+        side: 0,
+        block:1,//块排序
+        title: "Receive R$100 for free",
+        icon:"",
+        fill:";;",//icon 颜色 默认 选中 本身颜色
+        component:"cashwheel",
+        login:0,//0,1 检不检查登录
+        lpath:"#/signin",//登录path
+        subs:[]
+    },
 ]

+ 35 - 3
src/features/api.js

@@ -5,7 +5,7 @@ import {
     APP_CHANGE_INFO,
     APP_CHANGE_PASS,
     APP_CHANGE_PAY_PASS,
-    APP_CHANGE_PHONE,
+    APP_CHANGE_PHONE, APP_CUSTOMER_LEAVEMSG,
     APP_CUSTOMER_SERVICE,
     APP_FIRSTPAY_CONFIG,
     APP_GAME_INFO,
@@ -16,7 +16,7 @@ import {
     APP_PAYMENT_ORDER,
     APP_RECHARGE_CONFIG,
     APP_REGISTER,
-    APP_ROUTES,
+    APP_ROUTES, APP_TURNPLATE_INDEX,
     APP_WITHDRAW_BASE_INFO,
     APP_WITHDRAW_INFO,
     APP_WITHDRAW_TAX,
@@ -26,7 +26,10 @@ import Http from "@api/request";
 import {updateGameList} from "@features/pageSlice";
 import {APP_CHANNEL} from "@constants/app";
 import {updateInsurePass, updateUserInfo} from "@features/mainSlice";
-
+import {localStorageGet} from "@utils/helpers";
+import {LOCAL_CUSTOM_LOCK} from "@constants/localkey";
+import {toast} from "react-toastify";
+const i18n=global.i18n;
 export const buildReqReducer=(builder,thunk,callback,statusCB=null)=>{
     if (builder==null||thunk==null)return;
     builder
@@ -327,4 +330,33 @@ export const reqCustomService=createAsyncThunk(
         return response;
     }
 )
+export const reqCustomLeaveMsg=createAsyncThunk(
+    APP_CUSTOMER_LEAVEMSG,
+    async (arg,thunkAPI)=>{
+        let preTime=localStorageGet(LOCAL_CUSTOM_LOCK,"0");
+        let now=Date.now();
+        preTime=Number(preTime);
+        if (now-preTime<180000){
+            toast.error(i18n.t("customer.TopTip_timerFail"));
+            return null;
+        }
+        let params={
+            url:APP_CUSTOMER_LEAVEMSG,
+            params:arg
+        }
+        const response=await Http.get(params);
+        return response;
+    }
+)
+//Turnplate
+export const reqTurnplateIndex=createAsyncThunk(
+    APP_TURNPLATE_INDEX,
+    async (arg,thunkAPI)=>{
+        let params={
+            url:APP_TURNPLATE_INDEX
+        }
+        const response=await Http.get(params);
+        return response;
+    }
+)
 //api

+ 23 - 3
src/features/mainSlice.js

@@ -11,7 +11,7 @@ import {REQ_STATUS} from "@api/config";
 import {
     buildReqReducer, reqChangeEmail,
     reqChangeInfo,
-    reqChangePass, reqChangePhone, reqCustomService, reqGameInfo,
+    reqChangePass, reqChangePhone, reqCustomLeaveMsg, reqCustomService, reqGameInfo,
     reqGetRoutes,
     reqLogin,
     reqLogout,
@@ -19,7 +19,7 @@ import {
 } from "@features/api";
 import {toast} from 'react-toastify';
 import {getHttpMsg, localStorageGet, localStorageSet} from "@utils/helpers";
-import {LOCAL_IS_LOGIN, LOCAL_SIGN, LOCAL_USER_INFO} from "@constants/localkey";
+import {LOCAL_CUSTOM_LOCK, LOCAL_IS_LOGIN, LOCAL_SIGN, LOCAL_USER_INFO} from "@constants/localkey";
 import {reqPayConfig} from "@features/paySlice";
 const i18n=global.i18n;
 const generateNavs=(routes)=>{
@@ -285,10 +285,25 @@ const gameInfoStatus=(state,status,arg)=>{
 //custom
 const reducerCustom=(state,action,arg)=>{
     let payload=action.payload;
-    if (payload&&payload.code==200){
+    if (payload&&payload.code===200){
         state.customerInfo=payload.data;
     }
 }
+const reducerCustomLeaveMsg=(state,action,arg)=>{
+    let payload=action.payload;
+    if (payload!=null){
+        if (payload&&payload.code===200){
+            localStorageSet(LOCAL_CUSTOM_LOCK,Date.now());
+            toast.success(i18n.t("customer.TopTip_succeed"));
+        }
+        else {
+            toast.error(i18n.t("customer.TopTip_fail"));
+        }
+    }
+}
+const customLeaveMsgStatus=(state,status,arg)=>{
+    state.customLeaveMsgStatus=status;
+}
 //custom
 export const Main=createSlice({
     name: 'main',
@@ -313,6 +328,7 @@ export const Main=createSlice({
             email:REQ_STATUS.NONE
         },
         gameInfoStatus:REQ_STATUS.NONE,
+        customLeaveMsgStatus:REQ_STATUS.NONE,
         customerInfo:[]
     },
     reducers: {
@@ -339,6 +355,7 @@ export const Main=createSlice({
         buildReqReducer(builder,reqChangeEmail,reducerChangeEmail,changeEmailStatus);
         buildReqReducer(builder,reqGameInfo,reducerGameInfo,gameInfoStatus);
         buildReqReducer(builder,reqCustomService,reducerCustom,null);
+        buildReqReducer(builder,reqCustomLeaveMsg,reducerCustomLeaveMsg,customLeaveMsgStatus);
     },
 })
 export const {logout,updateUserInfo} = Main.actions;
@@ -486,6 +503,9 @@ export const getCustomTypeInfo=(type)=>(state)=>{
     }
     return item
 }
+export const getCustomStatus=(state)=>{
+    return state.main.customLeaveMsgStatus;
+}
 //数据获取
 export default Main.reducer;
 

+ 36 - 0
src/features/turnplateSlice.js

@@ -0,0 +1,36 @@
+import {createSlice} from "@reduxjs/toolkit";
+import {buildReqReducer, reqGameList, reqTurnplateIndex} from "@features/api";
+import {REQ_STATUS} from "@api/config";
+const reducerTurnplateIndex=(state,action,arg)=>{
+    let payload=action.payload;
+    if (payload&&payload.code===200){
+        let data=payload.data;
+        if (data){
+            state.turnplate=data.turnplate;
+            state.record=data.record;
+            state.link=data.link;
+            state.numbers=data.numbers;
+            state.myRecord=data.myRecord;
+        }
+    }
+}
+export const Turnplate=createSlice({
+    name: 'turnplate',
+    initialState: {
+        turnplate:null,
+        record:[],
+        link:null,
+        numbers:[],
+        myRecord:[]
+    },
+    reducers: {
+    },
+    extraReducers: (builder) => {
+        buildReqReducer(builder,reqTurnplateIndex,reducerTurnplateIndex,null);
+    },
+})
+export const {} = Turnplate.actions;
+export const getTurnplate=(state)=>{
+    return state.turnplate.turnplate;
+}
+export default Turnplate.reducer;

二进制
src/fonts/swisscbt/SWISSC-BT.ttf


+ 4 - 0
src/fonts/swisscbt/style.css

@@ -0,0 +1,4 @@
+@font-face {
+    font-family: 'SWISSC-BT';
+    src: url('../../fonts/swisscbt/SWISSC-BT.ttf')
+}

+ 2 - 2
src/layout/BottomNav/index.jsx

@@ -60,13 +60,13 @@ const BottomNav = () => {
     // )
     return (
         <div className={styles.container}>
-            <button className={styles.button}>
+            <NavLink className={styles.button} to={ROUTE_PATH.PROMOTION}>
                 {/*<svg className={"icon"}>*/}
                 {/*    <use xlinkHref={`${ticon_gift}#gift`} />*/}
                 {/*</svg>*/}
                 <TabGift className={styles.icon}/>
                 <span>{i18n.t("tab.promotion")}</span>
-            </button>
+            </NavLink>
             <NavLink className={styles.button} to={ROUTE_PATH.SAQUE} aria-label="Home">
                 {/*<svg className={"icon"}>*/}
                 {/* <use xlinkHref={`${symbol_m}#icon_Home_1`} />*/}

+ 8 - 0
src/locales/languages/en_US.json

@@ -284,5 +284,13 @@
     "whats_label": "Or copy the number to your WhatsApp",
     "email_label": "You can also contact us by email",
     "write_now": "Write Now"
+  },
+  "form_errors": {
+    "phone_55": "Enter a correct 11-digit phone number.",
+    "pass": "The password must not be less than 6 characters"
+  },
+  "turnplate": {
+    "expires_in": "Expires in",
+    "invite_tips": "Invite friends to help with withdrawals"
   }
 }

+ 10 - 0
src/pages/CashWheel.jsx

@@ -0,0 +1,10 @@
+import WCashWheel from "@widgets/WCashWheel";
+
+const CashWheel=({...props})=>{
+    return (
+        <>
+            <WCashWheel {...props}/>
+        </>
+    )
+}
+export default CashWheel;

+ 10 - 0
src/pages/Promotion.jsx

@@ -0,0 +1,10 @@
+import WPromotion from "@widgets/WPromotion";
+
+const Promotion=({...props})=>{
+    return (
+        <>
+            <WPromotion {...props}/>
+        </>
+    )
+}
+export default Promotion;

+ 5 - 1
src/pages/index.jsx

@@ -14,6 +14,8 @@ const Withdraw=lazy(()=>import("@pages/Withdraw"));
 const WithdrawAccountEdit=lazy(()=>import("@pages/WithdrawAccountEdit"));
 const WithdrawPassEdit=lazy(()=>import("@pages/WithdrawPassEdit"));
 const CustomerService=lazy(()=>import("@pages/CustomerService"));
+const Promotion=lazy(()=>import("@pages/Promotion"));
+const CashWheel=lazy(()=>import("@pages/CashWheel"));
 export default {
     ModulePage:ModulePage,
     Home:Home,
@@ -27,7 +29,9 @@ export default {
     withdraw:Withdraw,
     accountInfo:WithdrawAccountEdit,
     wdPassEdit:WithdrawPassEdit,
-    customer:CustomerService
+    customer:CustomerService,
+    promotion:Promotion,
+    cashwheel:CashWheel
 }
 export const Header={
     deposit: DepositHeader,

+ 2 - 1
src/style.scss

@@ -6,4 +6,5 @@
 @import "styles/elements";
 @import "styles/typo";
 @import "styles/rtl";
-@import "fonts/icomoon/style.css";
+@import "fonts/icomoon/style.css";
+@import "fonts/swisscbt/style.css";

+ 3 - 0
src/styles/_elements.scss

@@ -700,6 +700,9 @@ a {
         }
     }
 }
+.input-prompt-error{
+    color: var(--red);
+}
 .button-green{
     display: flex;
     align-items: center;

+ 37 - 0
src/styles/_keyframes.scss

@@ -24,3 +24,40 @@
 .rotate-center {
     animation: rotate-center 3s ease-in-out infinite;
 }
+@keyframes iconshake {
+    0%, 65% {
+        transform: rotate(0deg);
+        -webkit-transform: rotate(0deg);
+    }
+    70% {
+        transform: rotate(15deg);
+        -webkit-transform: rotate(15deg);
+    }
+    75% {
+        transform: rotate(-15deg);
+        -webkit-transform: rotate(-15deg);
+    }
+    80% {
+        transform: rotate(15deg);
+        -webkit-transform: rotate(15deg);
+    }
+    85% {
+        transform: rotate(-15deg);
+        -webkit-transform: rotate(-15deg);
+    }
+    90% {
+        transform: rotate(15deg);
+        -webkit-transform: rotate(15deg);
+    }
+    95% {
+        transform: rotate(-15deg);
+        -webkit-transform: rotate(-15deg);
+    }
+    100% {
+        transform: rotate(0deg);
+        -webkit-transform: rotate(0deg);
+    }
+}
+.iconshake{
+    animation: 3s ease-in-out 0s infinite normal none running iconshake;
+}

+ 3 - 0
src/styles/_utils.scss

@@ -254,3 +254,6 @@
 .card-padded {
     padding: var(--card-padding);
 }
+.swissc_bt_txt{
+    font-family: "SWISSC-BT";
+}

+ 12 - 0
src/utils/helpers.js

@@ -214,3 +214,15 @@ export const openFullScreen=()=>{
         elem.msRequestFullscreen();
     }
 }
+export const validateBRPhone=(t)=>{
+    if (t && 11 === t.length) {
+        var e = parseInt(t.substring(0, 2), 10)
+            , i = parseInt(t.charAt(2), 10);
+        return e >= 11 && e <= 99 && 9 == i
+    }
+    return false;
+
+}
+export const validateCountryPhone=(country,phone)=>{
+    return validateBRPhone(phone);
+}

+ 23 - 13
src/widgets/LoginForm/index.jsx

@@ -27,6 +27,7 @@ import {REQ_STATUS} from "@api/config";
 import {reqLogin, reqRegister} from "@features/api";
 import {ROUTE_PATH} from "@constants/routes";
 import {APP_THEME} from "@constants/app";
+import {validateCountryPhone} from "@utils/helpers";
 
 const i18n=global.i18n;
 const TYPE_LOGIN="login";
@@ -93,7 +94,9 @@ const LoginForm = ({close}) => {
         data.callback=onCallback;
         dispatch(reqRegister(data));
     }
-
+    const validatePhone=(value)=>{
+        return validateCountryPhone(country,value);
+    }
     const renderLoading=()=>{
         return (<SmallLoading fill={"#fff"}/>)
     }
@@ -114,25 +117,28 @@ const LoginForm = ({close}) => {
                     <Controller control={control}
                                 name="account"
                                 rules={{required: true}}
-                                render={({field: {ref, onChange, value}, fieldState: {error}}) => (
-                                    <PhoneInput
+                                render={({field: {ref, onChange, value}, fieldState: {error}}) => {
+                                    let message=error?i18n.t("form_errors.phone_"+country):"";
+                                    return (<PhoneInput
                                         className={classNames({'input-control--error': error})}
                                         value={value}
+                                        message={message}
                                         onChange={e => onChange(e.target.value)}
                                         onChangeCountry={(country)=>{
                                             setCountry(country);
                                         }}
                                         placeholder={i18n.t('login.phone_ph')}
-                                        innerRef={ref}/>
-                                )}
+                                        innerRef={ref}/>)
+                                }}
                     />
                     <Controller control={control}
                                 name="password"
-                                rules={{required: true}}
+                                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.login_pwd')}
                                         innerRef={ref}/>
@@ -171,26 +177,29 @@ const LoginForm = ({close}) => {
                 })}>
                     <Controller control={control}
                                 name="account"
-                                rules={{required: true}}
-                                render={({field: {ref, onChange, value}, fieldState: {error}}) => (
-                                    <PhoneInput
+                                rules={{required: true,validate:validatePhone}}
+                                render={({field: {ref, onChange, value}, fieldState: {error}}) => {
+                                    let message=error?i18n.t("form_errors.phone_"+country):"";
+                                    return (<PhoneInput
                                         className={classNames({'input-control--error': error})}
                                         value={value}
+                                        message={message}
                                         onChange={e => onChange(e.target.value)}
                                         onChangeCountry={(country)=>{
                                             setCountry(country);
                                         }}
                                         placeholder={i18n.t('login.phone_ph')}
-                                        innerRef={ref}/>
-                                )}
+                                        innerRef={ref}/>)
+                                }}
                     />
                     <Controller control={control}
                                 name="password"
-                                rules={{required: true}}
+                                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.login_pwd')}
                                         innerRef={ref}/>
@@ -198,11 +207,12 @@ const LoginForm = ({close}) => {
                     />
                     <Controller control={control}
                                 name="repassword"
-                                rules={{required: true}}
+                                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}/>

+ 288 - 0
src/widgets/WCashWheel/WWheel/WWheel.jsx

@@ -0,0 +1,288 @@
+import styles from "./styles.module.scss";
+import d_bg from "@assets/wheel/d.png";
+import bg_light from "@assets/wheel/bg_light.png";
+import light1 from "@assets/wheel/light1.png";
+import light2 from "@assets/wheel/light2.png";
+import spinBg from "@assets/wheel/spinBg.png";
+import pointer from "@assets/wheel/pointer.png"
+import styled from 'styled-components/macro';
+import {useEffect, useRef, useState} from "react";
+import spin_1 from "@assets/wheel/spin-1.png";
+import spin_2 from "@assets/wheel/spin-2.png";
+import spin_4 from "@assets/wheel/spin-4.png";
+import spin_5 from "@assets/wheel/spin-5.png";
+import draw from "@assets/wheel/draw.png";
+import {useSelector} from "react-redux";
+import {getTurnplate} from "@features/turnplateSlice";
+import {formatTime} from "@utils/helpers";
+import SvgIcon from "@components/SvgIcon";
+import {ReactComponent as ShareIcon} from '@assets/singlesvg/share.svg';
+const i18n=global.i18n;
+const spinImgSrc=[
+    null,
+    spin_1,
+    spin_2,
+    null,
+    spin_4,
+    spin_5
+]
+const spinImg=[];
+const StyledCanvas=styled.canvas`
+    background-image:url('${spinBg}');
+`
+const turnObj={
+    timer:null,
+    cdTimer:null,
+    count:0
+};
+const WWheel=()=>{
+    let turnplate=useSelector(getTurnplate);
+    if(turnplate==null)turnplate={times:0};
+    const data=[
+        {id:0,type: 1,value: 0},
+        {id:1,type: 0,value: 1000},
+        {id:2,type: 0,value: 50},
+        {id:3,type: 5,value: 0},
+        {id:4,type: 1,value: 0},
+        {id:5,type: 4,value: 0},
+        {id:6,type: 2,value: 0},
+        {id:7,type: 0,value: 1},
+    ]
+    const drawRef=useRef(null);
+    const [imgLoaded,setImgLoaded]=useState(false);
+    const dataLen=8;
+    const startRotateDegree=3240;
+    const startRotateDegreeDynamic=3240;
+    const [drawStyle,setDrawStyle]=useState({});
+    const [countdown,setCountdown]=useState(turnplate?turnplate.countdown:0);
+    const loadImgs=()=>{
+        let totalCount=spinImgSrc.length;
+        let loadedCount=0;
+        let checkDone=()=>{
+            if (loadedCount===totalCount){
+                setImgLoaded(true);
+            }
+        }
+        for (let i = 0; i < totalCount; i++) {
+            let imgSrc=spinImgSrc[i];
+            if (imgSrc==null){
+                loadedCount++;
+                spinImg[i]=null;
+                checkDone();
+            }
+            else{
+                let img=new Image;
+                img.src=imgSrc;
+                spinImg[i]=img;
+                img.onload=()=>{
+                    loadedCount++;
+                    checkDone();
+                }
+            }
+        }
+    }
+    const clearCDTimer=()=>{
+        if (turnObj.cdTimer!=null){
+            clearInterval(turnObj.cdTimer);
+            turnObj.cdTimer=null;
+        }
+    }
+    const updateCD=()=>{
+        if (countdown<=0){
+            clearCDTimer();
+            return;
+        }
+        setCountdown(countdown-1);
+    }
+    const checkCountdown=()=>{
+        if (countdown>0){
+            clearCDTimer();
+            let left=countdown;
+            turnObj.cdTimer=setInterval(()=>{
+                left--;
+                if (left<=0){
+                    clearCDTimer();
+                    setCountdown(0);
+                    return;
+                }
+                setCountdown(left);
+            },1000);
+        }
+    }
+    const startSpin=()=>{
+        // drawAnimation();
+        startTurn();
+    }
+    const clearTurnTimer=()=>{
+        if (turnObj.timer!=null){
+            clearInterval(turnObj.timer);
+            turnObj.timer=null;
+        }
+    }
+    const startTurn=()=>{
+        clearTurnTimer();
+        turnObj.count=0;
+        let turnCount=turnObj.count;
+        let during=0.333;
+        let roteStyle={
+            transform:`rotate(360deg)`,
+            transition:`transform ${during*2}s cubic-bezier(0.42, 0.00, 1.00, 1.00)`,
+        }
+        setDrawStyle(roteStyle);
+        let timer=setInterval(()=>{
+            turnCount++;
+            turnObj.count++;
+            roteStyle={
+                transform:`rotate(${turnCount*360}deg)`,
+                transition:`transform ${during}s cubic-bezier(1.00, 1.00, 0.00, 0.00)`,
+            }
+            setDrawStyle(roteStyle);
+            if(turnCount>3){
+                // drawAnimation(turnCount*360+startRotateDegreeDynamic);
+                stopTurn(1);
+            }
+        },during*1000);
+        turnObj.timer=timer;
+    }
+    const stopTurn=(reward)=>{
+        let index=0;
+        for (let i = 0; i < dataLen; i++) {
+            let item=data[i];
+            if (item&&item.id===reward){
+                index=i;
+                break;
+            }
+        }
+        let per=360/dataLen;
+        let angle=turnObj.count*360+startRotateDegreeDynamic-(per*index);
+        drawAnimation(angle);
+    }
+    const drawAnimation=(angle=startRotateDegreeDynamic)=>{
+        clearTurnTimer();
+        let roteStyle={
+            transform:"rotate(".concat(angle, "deg)"),
+            transition:"transform 5.5s cubic-bezier(0.250, 0.460, 0.455, 0.995)",
+        };
+        setDrawStyle(roteStyle);
+    }
+    const drawWheel=()=>{
+        let draw=drawRef.current;
+        if (draw==null)return;
+        let colors=["#B90615","#FFD760"];
+        let color_len=colors.length;
+        let fontSizeNum=25;
+        if (data!=null){
+            let s=draw;
+            let rdat=data.slice(0,dataLen);
+            let c=draw.getContext("2d");
+            let dcwidth=draw.clientWidth;
+            let dcheight=draw.clientHeight;
+            let devicePixelRatioNum = window.devicePixelRatio < 1 ? 1 : window.devicePixelRatio;
+            s.width=dcwidth*devicePixelRatioNum;
+            s.height=dcheight*devicePixelRatioNum;
+            c.clearRect(0, 0, s.width, s.height);
+            let l = 2 * Math.PI / dataLen;
+            let g = -s.width / 2 + s.width / 2 / 2.5;
+            c.translate(s.width / 2, s.width / 2);
+            c.rotate(-2*l);
+            c.save();
+            for (let i = 0; i < dataLen; i++) {
+                let item=rdat[i];
+                if (item==null)continue;
+                let color=colors[i%color_len];
+                c.rotate(l);
+                c.save();
+                c.beginPath();
+                c.fillStyle = color;
+                c.textBaseline = "middle";
+                c.textAlign = "center";
+                c.font = "".concat(devicePixelRatioNum * fontSizeNum, "px SWISSC-BT");
+                c.translate(s.width / 5.2, -s.width / dataLen);
+                c.rotate(.8);
+                c.translate(-s.width / 5.2, s.width / dataLen);
+                if (item.type===0){
+                    c.fillText(item.value,s.width / 7.2, g + devicePixelRatioNum * fontSizeNum * .36, 3 * s.width / dataLen);
+                }
+                else{
+                    let img=spinImg[item.type];
+                    if(img)c.drawImage(img, s.width / 23, g - 11 * devicePixelRatioNum, s.width / 5, s.width / 4.37);
+                }
+                c.restore();
+            }
+        }
+    }
+    useEffect(()=>{
+        loadImgs();
+        return ()=>{
+            clearTurnTimer();
+            clearCDTimer();
+        }
+    },[]);
+    useEffect(()=>{
+        if (imgLoaded){
+            drawWheel();
+        }
+    },[imgLoaded])
+    useEffect(()=>{
+        checkCountdown();
+    },[turnplate.countdown])
+    const renderSpinInfo=()=>{
+        let times=turnplate?turnplate.times:0;
+        return (
+            <>
+                <div className={styles.spin_con}>
+                    <button className={styles.spin_btn} onClick={()=>{
+                        startSpin();
+                    }}>
+                        <img src={draw} className={styles.spin_img}/>
+                        <div className={styles.spin_content}>
+                            <div className={styles.spin_num}>{times}</div>
+                        </div>
+                    </button>
+                </div>
+            </>
+        )
+    }
+    const renderCountDown=()=>{
+        let d=Math.floor(countdown/86400);
+        let time=countdown-d*86400;
+        let stime=formatTime(time);
+        return (
+            <div className={styles.cd_con}>
+                <div>{i18n.t("turnplate.expires_in")}</div>
+                <div className={styles.text}>{d+"d"}</div>
+                <div className={styles.text}>{stime}</div>
+            </div>
+        )
+    }
+    const renderShareBar=()=>{
+        return (
+            <button className={styles.share_con}>
+                <div className={styles.share_bar}>
+                    <div className={styles.text}>{i18n.t("turnplate.invite_tips")}</div>
+                    <ShareIcon className={styles.icon}/>
+                </div>
+            </button>
+        )
+    }
+    return (
+        <div className={styles.container}>
+            <div className={styles.wheel}>
+                <img src={bg_light} className={styles.bg_light}/>
+                <img src={d_bg} className={styles.lamp}/>
+                <div className={styles.drawContent}>
+                    <StyledCanvas ref={drawRef} className={styles.draw} style={drawStyle}/>
+                </div>
+                <img src={light1} className={`${styles.light} ${styles.light_ani_1}`}/>
+                <img src={light2} className={`${styles.light} ${styles.light_ani_2}`}/>
+                <img src={pointer} className={styles.pointer}/>
+                {
+                    renderSpinInfo()
+                }
+            </div>
+            {renderCountDown()}
+            {renderShareBar()}
+        </div>
+    )
+}
+export default WWheel;

+ 148 - 0
src/widgets/WCashWheel/WWheel/styles.module.scss

@@ -0,0 +1,148 @@
+.container{
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  position: relative;
+  width: 100%;
+}
+@keyframes bg_light_ani {
+  0%{
+    transform: rotate(0deg);
+  }
+  100%{
+    transform: rotate(1turn);
+  }
+}
+@keyframes breathe {
+  0% { opacity: 1; }
+  50% { opacity: 1; }
+  51% { opacity: 0; }
+  100% { opacity: 0; }
+}
+.wheel{
+  display: flex;
+  position: relative;
+  align-items: center;
+  justify-content: center;
+  width: 100%;
+  aspect-ratio:1;
+  & .bg_light{
+    position: absolute;
+    z-index: 0;
+    animation: bg_light_ani 20s linear infinite;
+    transform: translateZ(0);
+  }
+  & .lamp{
+    position: absolute;
+    right: -1%;
+    width: 101%;
+    height: auto;
+    z-index: 1;
+  }
+  & .drawContent{
+    position: absolute;
+    top: 29%;
+    width: 55%;
+    aspect-ratio:1;
+    z-index: 2;
+  }
+  & .draw{
+    width: 100%;
+    height: 100%;
+    background-size: cover;
+    border-radius: 50%;
+    font-family: "SWISSC-BT";
+    font-size: 1.56rem;
+  }
+  & .light{
+    position: absolute;
+    top: 25%;
+    width: 62%;
+    height: auto;
+    z-index: 3;
+    animation: breathe 1.2s infinite;
+  }
+  & .pointer{
+    position: absolute;
+    z-index: 4;
+    width: 21%;
+    top: 23%;
+  }
+  & .light_ani_1{
+    animation-delay: 0s;
+  }
+  & .light_ani_2{
+    animation-delay: 0.6s;
+  }
+  & .spin_con{
+    position: absolute;
+    z-index: 5;
+    width: 18%;
+    top: 45%;
+  }
+  & .spin_btn{
+    position: relative;
+    width: 100%;
+    & .spin_img{
+
+    }
+    & .spin_content{
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      position: absolute;
+      z-index: 1;
+      top: 23%;
+      right: 0;
+      left: 0;
+      bottom: 0;
+      font-family: "SWISSC-BT";
+      color: #fff;
+    }
+    & .spin_num{
+      font-size: 2.3rem;
+      text-shadow:
+              -1.5px -1.5px 0 #953500,
+              1.5px -1.5px 0 #953500,
+              -1.5px  1.5px 0 #953500,
+              1.5px  1.5px 0 #953500;
+    }
+  }
+}
+.cd_con{
+  display: flex;
+  align-items: center;
+  gap: 0.5rem;
+  margin-top: -1rem;
+  color: rgb(135, 149, 177);
+  font-size: 0.9rem;
+  font-weight: 600;
+  & .text{
+    color:var(--header)
+  }
+}
+.share_con{
+  padding: 1rem;
+  width: 100%;
+}
+.share_bar{
+  display: flex;
+  flex-direction: row;
+  background-color: var(--standard);
+  border-radius: var(--border-radius);
+  padding: 0.5rem 1rem;
+  width: 100%;
+  justify-content: space-between;
+  align-items: center;
+  & .icon{
+    fill: var(--header);
+    width: 1.6rem;
+    height: 1.6rem;
+  }
+  & .text{
+    font-size: 1rem;
+    color: var(--header);
+    text-align: center;
+    width: 100%;
+  }
+}

+ 26 - 0
src/widgets/WCashWheel/index.jsx

@@ -0,0 +1,26 @@
+import styles from "./styles.module.scss";
+import WWheel from "@widgets/WCashWheel/WWheel/WWheel";
+import Spring from "@components/Spring";
+import cash_2 from "@assets/wheel/cash_2.png";
+const WCashWheel=({close})=>{
+    const renderCashMain=()=>{
+        return (
+            <div className={styles.cash_main_con}>
+                <div className={styles.cash_main}>
+                    <div className={styles.have_cash}>
+                        <img className={styles.cash_img} src={cash_2}/>
+                    </div>
+                </div>
+            </div>
+        )
+    }
+    return (
+        <div className={styles.container}>
+            <Spring type={"slideUp"} className={styles.content}>
+                {renderCashMain()}
+                <WWheel></WWheel>
+            </Spring>
+        </div>
+    )
+}
+export default WCashWheel;

+ 38 - 0
src/widgets/WCashWheel/styles.module.scss

@@ -0,0 +1,38 @@
+.container{
+  width: 100%;
+  min-height: 100%;
+  display: flex;
+  flex-direction: column;
+  position: relative;
+  background-color: var(--widget-bg);
+}
+.content{
+  display: flex;
+  flex-direction: column;
+  position: relative;
+  width: 100%;
+  gap: 1rem;
+}
+.cash_main_con{
+  width: 100%;
+  padding: 1rem;
+}
+.cash_main{
+  display: flex;
+  flex-direction: column;
+  padding: 1rem;
+  align-items: center;
+  justify-content: center;
+  background-color: var(--standard);
+  border-radius: 0.8rem;
+  & .have_cash{
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    & .cash_img{
+      width: 4rem;
+      height: auto;
+    }
+  }
+}
+

+ 26 - 5
src/widgets/WCustomerService/index.jsx

@@ -4,11 +4,15 @@ import {ReactComponent as WSIcon} from '@assets/singlesvg/whatsapp.svg';
 import {ReactComponent as TGIcon} from '@assets/singlesvg/telegram.svg';
 import {ReactComponent as EamilIcon} from '@assets/singlesvg/email-open.svg';
 import SvgIcon from "@components/SvgIcon";
-import {useSelector} from "react-redux";
-import {getCustomTypeInfo, getUserInfo} from "@features/mainSlice";
+import {useDispatch, useSelector} from "react-redux";
+import {getCustomStatus, getCustomTypeInfo, getUserInfo} from "@features/mainSlice";
 import {toast} from "react-toastify";
 import {CopyToClipboard} from "react-copy-to-clipboard";
 import {APP_CHANNEL} from "@constants/app";
+import SmallLoading from "@components/SmallLoading";
+import {useState} from "react";
+import {REQ_STATUS} from "@api/config";
+import {reqCustomLeaveMsg} from "@features/api";
 const i18n=global.i18n;
 const WCustomerService=({close})=>{
     const whatsInfo=useSelector(getCustomTypeInfo("WhatsApp"));
@@ -16,6 +20,9 @@ const WCustomerService=({close})=>{
     const tgInfo=useSelector(getCustomTypeInfo("Telegram"));
     const user=useSelector(getUserInfo);
     const gameID=user?user.GameID:"";
+    const status=useSelector(getCustomStatus);
+    const dispatch=useDispatch();
+    const [leaveMsg,setLeaveMsg]=useState("");
     const onClickWS=()=>{
         if (whatsInfo){
             let url=whatsInfo.StatusString+"&gameid="+gameID;
@@ -32,6 +39,16 @@ const WCustomerService=({close})=>{
         let url="mailto:"+email+"?subject=Mexico "+APP_CHANNEL.Channel+" gameid:"+gameID+"&body=problem:";
         window.open(url,"_blank");
     }
+    const onSubmit=()=>{
+        if (leaveMsg==="")return;
+        if (status===REQ_STATUS.LOADING)return;
+        let data={
+            message:encodeURI(leaveMsg),
+            GameID:gameID,
+            PackageName:APP_CHANNEL.PackageName
+        }
+        dispatch(reqCustomLeaveMsg(data));
+    }
     const renderTips=()=>{
         return (
             <div className={styles.tips}>
@@ -135,10 +152,14 @@ const WCustomerService=({close})=>{
                 </div>
                 <div className={styles.line}></div>
                 <div className={styles.report_con}>
-                    <textarea rows={7} maxLength={300} placeholder={i18n.t("customer.input_holder")}/>
+                    <textarea value={leaveMsg} rows={7} maxLength={300} placeholder={i18n.t("customer.input_holder")} onChange={(e)=>{
+                        setLeaveMsg(e.target.value);
+                    }}/>
                 </div>
-                <button className={"button-green"}>
-                    {i18n.t("customer.Btn_send")}
+                <button className={"button-green"} onClick={onSubmit}>
+                    {
+                        status===REQ_STATUS.LOADING?(<SmallLoading fill={"#fff"} />):(<>{i18n.t("customer.Btn_send")}</>)
+                    }
                 </button>
             </div>
         )

+ 6 - 3
src/widgets/WEditPassword/index.jsx

@@ -53,11 +53,12 @@ const WEditPassword=({close})=>{
                 })}>
                     <Controller control={control}
                                 name="oldpassword"
-                                rules={{required: true}}
+                                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('user.password.oldpassword')}
                                         innerRef={ref}/>
@@ -65,11 +66,12 @@ const WEditPassword=({close})=>{
                     />
                     <Controller control={control}
                                 name="newpassword"
-                                rules={{required: true}}
+                                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('user.password.newpassword')}
                                         innerRef={ref}/>
@@ -77,11 +79,12 @@ const WEditPassword=({close})=>{
                     />
                     <Controller control={control}
                                 name="renewpassword"
-                                rules={{required: true}}
+                                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('user.password.renewpassword')}
                                         innerRef={ref}/>

+ 20 - 12
src/widgets/WEditProfile/index.jsx

@@ -13,6 +13,7 @@ import {ReactComponent as FeMale} from '@assets/singlesvg/gener/female.svg';
 import SmallLoading from "@components/SmallLoading";
 import {reqChangeEmail, reqChangeInfo, reqChangePhone} from "@features/api";
 import {APP_GENDER} from "@constants/app";
+import {validateCountryPhone} from "@utils/helpers";
 const i18n=global.i18n;
 const getAvatarUrl=(id)=>{
     return `${APP_AVATAR}avatar_${id}.png`;
@@ -49,6 +50,9 @@ const WEditProfile=({close})=>{
     const dispatch = useDispatch();
     const [avatarID,setAvatarID]=useState(user.FaceID);
     const [country, setCountry] = useState(user.RegisterLocation);
+    const validatePhone=(value)=>{
+        return validateCountryPhone(country,value);
+    }
     const getPhone=()=>{
         let loc=user.RegisterLocation;
         let phone=user.Phone;
@@ -127,19 +131,23 @@ const WEditProfile=({close})=>{
                 <div className={styles.title}>{i18n.t("user.title_phone")}</div>
                 <Controller control={control}
                             name="Phone"
-                            rules={{required: true}}
-                            render={({field: {ref, onChange, value}, fieldState: {error}}) => (
-                                <PhoneInput
-                                    className={classNames({'input-control--error': error})}
-                                    value={value}
-                                    onChange={e => onChange(e.target.value)}
-                                    onChangeCountry={(country)=>{
-                                        setCountry(country);
-                                    }}
-                                    placeholder={i18n.t('user.phone_ph')}
-                                    innerRef={ref}/>
+                            rules={{required: true,validate:validatePhone}}
+                            render={({field: {ref, onChange, value}, fieldState: {error}}) => {
+                                let message=error?i18n.t("form_errors.phone_"+country):"";
+                                return (
+                                    <PhoneInput
+                                        className={classNames({'input-control--error': error})}
+                                        value={value}
+                                        message={message}
+                                        onChange={e => onChange(e.target.value)}
+                                        onChangeCountry={(country)=>{
+                                            setCountry(country);
+                                        }}
+                                        placeholder={i18n.t('user.phone_ph')}
+                                        innerRef={ref}/>
 
-                            )}
+                                )
+                            }}
                 />
             </div>
         )

+ 62 - 0
src/widgets/WPromotion/index.jsx

@@ -0,0 +1,62 @@
+import styles from './styles.module.scss';
+import Spring from "@components/Spring";
+import React from "react";
+import icon_1 from "@assets/promotion/icon_1.svg";
+import banner_1 from "@assets/promotion/banner_1.png";
+import {useNavigate} from "react-router-dom";
+const icons=[
+    null,
+    icon_1
+]
+const banners=[
+    null,
+    banner_1
+]
+const WPromotion=({close})=>{
+    const navigate=useNavigate();
+    let data=[
+        {
+            id:1,
+            title:"Tycoon",
+            icon:"",
+            banner:"",
+            link:"#/cashwheel",
+        }
+    ]
+    const renderList=()=>{
+        return (
+            <>
+                {
+                    data.map((item,index)=>{
+                        return (
+                            <div key={index} className={styles.gift_item}>
+                                <div className={styles.gift_title}>{item.title}</div>
+                                <button className={styles.git_fun_item} onClick={()=>{
+                                    navigate(item.link);
+                                }}>
+                                    <div className={styles.left}>
+                                        <div className={styles.icon_con}>
+                                            <img className={"iconshake"} src={icons[item.id]}/>
+                                            <div className={styles.red_dot}></div>
+                                        </div>
+                                    </div>
+                                    <div className={styles.right}>
+                                        <img src={banners[item.id]}/>
+                                    </div>
+                                </button>
+                            </div>
+                        )
+                    })
+                }
+            </>
+        )
+    }
+    return (
+        <div className={styles.container}>
+            <Spring type="slideUp" className={styles.content}>
+                {renderList()}
+            </Spring>
+        </div>
+    )
+}
+export default WPromotion;

+ 73 - 0
src/widgets/WPromotion/styles.module.scss

@@ -0,0 +1,73 @@
+.container{
+  width: 100%;
+  min-height: 100%;
+  display: flex;
+  flex-direction: column;
+  position: relative;
+  padding: 1rem .75rem;
+  background-color: var(--widget-bg);
+}
+.content{
+  display: flex;
+  flex-direction: column;
+  gap: 1rem;
+  margin-top: 0.5rem;
+}
+.gift_item{
+  display: flex;
+  flex-direction: column;
+  gap: 0.5rem;
+  & .gift_title{
+    font-size: 1.2rem;
+    font-weight: 500;
+    color: var(--header);
+  }
+  & .git_fun_item{
+    display: flex;
+    flex-direction: row;
+    align-items: center;
+    justify-content: space-between;
+    gap: 0.8rem;
+    & .left{
+      display: flex;
+      padding: 1.5rem 0.55rem;
+      align-items: center;
+      justify-content: center;
+      background-image: linear-gradient(135deg, #FCD7A1 0%, #D17836 100%);
+      box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.25);
+      border-radius: 0.5rem;
+      & .icon_con{
+        display: flex;
+        position: relative;
+        align-items: center;
+        justify-content: center;
+        width: 3.3rem;
+        height: 3.3rem;
+        border-radius: 50%;
+        background: #3F5B88;
+        border: 0.04rem solid #FCD7A1;
+        img{
+          height: 80%;
+          width: auto;
+        }
+        & .red_dot{
+          top: 0rem;
+          right: 0.2rem;
+          position: absolute;
+          width: 0.8rem;
+          height: 0.8rem;
+          border-radius: 50%;
+          background: #FF0000;
+        }
+      }
+    }
+    & .right{
+      width: 100%;
+      img{
+        border-radius: 0.5rem;
+        width: 100%;
+        height: auto;
+      }
+    }
+  }
+}

部分文件因为文件数量过多而无法显示