Ver Fonte

面包屑 loadmore

zhejiushizhuce há 2 anos atrás
pai
commit
5419d4cabd

+ 2 - 0
src/api/config.js

@@ -13,4 +13,6 @@ export const BASE_API="https://api.ouro777.com/";
 export const APP_ROUTES="game/routes";
 //page 模块
 export const APP_PAGE_MODULES="game/pageModules";
+//game more
+export const APP_GAME_LIST="game/gameList";
 

+ 3 - 1
src/app/store.js

@@ -2,11 +2,13 @@ import {configureStore} from '@reduxjs/toolkit';
 import TodosReducer from '../features/todos/todosSlice';
 import MainReducer from '@features/mainSlice';
 import PageReducer from '@features/pageSlice';
+import GameListReducer from '@features/gameListSlice';
 
 export default configureStore({
     reducer: {
         todos: TodosReducer,
         main:MainReducer,
-        page:PageReducer
+        page:PageReducer,
+        gamelist:GameListReducer
     }
 });

+ 1 - 1
src/components/GameItem/index.jsx

@@ -37,7 +37,7 @@ const GameItem=({data})=>{
             <div className={styles.bot_info}>
                 <div className={styles.game_tit}>
                     <NavLink className={styles.game_desc}>
-                        {data.desc}
+                        {data.brand}
                     </NavLink>
                     <button>
                         <svg className={`${styles.game_desc_icon}`}>

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

@@ -1,8 +1,10 @@
 import styles from "@components/GameList/styles.module.scss";
 import React from "react";
 import GameItem from "@components/GameItem";
+import {REQ_STATUS} from "@api/config";
+import SmallLoading from "@components/SmallLoading";
 const i18n =global.i18n;
-const GameList=({data})=>{
+const GameList=({data,status,reqMore})=>{
     // data={
     //     type:0,//游戏分类
     //     page:1,
@@ -100,11 +102,29 @@ const GameList=({data})=>{
             </div>
         )
     }
+    const renderLoading=()=>{
+        return (<SmallLoading fill={"#fff"}/>)
+    }
+    const onClickMore=()=>{
+        if (reqMore){
+            let page=Number(data.page)+1;
+            let tabtype=data.tabtype;
+            reqMore(tabtype,page);
+        }
+    }
     const renderShowMoreBtn=()=>{
+        let isNomore=Number(data.page)===Number(data.totalPage);
+        let label=isNomore?i18n.t("game_list.no_more"):i18n.t("game_list.show_more");
         return (
             <div className={styles.show_more}>
-                <button className={styles.more_btn}>
-                    <div>{i18n.t("game_list.show_more")}</div>
+                <button className={styles.more_btn} disabled={isNomore} onClick={()=>{
+                    if (status!=REQ_STATUS.LOADING){
+                        onClickMore();
+                    }
+                }}>
+                    {
+                        status===REQ_STATUS.LOADING?renderLoading():(<div>{label}</div>)
+                    }
                 </button>
             </div>
         )

+ 4 - 0
src/components/GameList/styles.module.scss

@@ -47,6 +47,10 @@
       margin: 0 auto;
       font-weight: 600;
       color: var(--header);
+      &:disabled{
+        opacity: .5;
+        cursor: default;
+      }
     }
   }
 }

+ 0 - 1
src/components/GameTabs/index.jsx

@@ -2,7 +2,6 @@ import styles from "@components/GameTabs/styles.module.scss";
 import React, {Fragment, useState} from "react";
 import symbol from '@assets/symbol-defs.svg';
 import AllWidgets from "@widgets/index";
-
 const GameTabs=({data})=>{
     const [tabIndex, setTabIndex] = useState(1);
     // data={

+ 14 - 0
src/components/SmallLoading/index.jsx

@@ -0,0 +1,14 @@
+import styles from './styles.module.scss';
+const SmallLoading=({fill = "#3BC117"})=>{
+    return (
+        <div className={styles.loading}>
+            <svg viewBox="0 0 84 24" fill={fill}>
+                <circle cx="18" cy="12" r="6"></circle>
+                <circle cx="18" cy="12" r="6"></circle>
+                <circle cx="42" cy="12" r="6"></circle>
+                <circle cx="66" cy="12" r="6"></circle>
+            </svg>
+        </div>
+    )
+}
+export default SmallLoading;

+ 60 - 0
src/components/SmallLoading/styles.module.scss

@@ -0,0 +1,60 @@
+.loading{
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  flex: 1;
+  height: 100%;
+  align-self: stretch;
+  box-sizing: border-box;
+  svg{
+    height: 40%;
+    max-height: 1.25rem;
+    max-width: 80%;
+    stroke: transparent;
+  }
+  circle{
+    transform-origin: center;
+    will-change: transform;
+    animation-timing-function: linear;
+    &:nth-child(1) {
+      animation: dot-1 .6s infinite;
+    }
+    &:nth-child(2) {
+      animation: dot-2 .6s infinite;
+    }
+    &:nth-child(3) {
+      animation: dot-2 .6s infinite;
+    }
+    &:nth-child(4) {
+      animation: dot-3 .6s infinite;
+    }
+  }
+}
+@keyframes dot-1 {
+  0% {
+    transform: scale(.4) translate(28.57%);
+    opacity:0.3;
+  }
+  100% {
+    transform: scale(1) translate(0);
+    opacity: 1;
+  }
+}
+@keyframes dot-2 {
+  0% {
+    transform: translate(0);
+  }
+  100% {
+    transform: translate(28.57%);
+  }
+}
+@keyframes dot-3 {
+  0% {
+    transform: scale(1) translate(0);
+    opacity: 1;
+  }
+  100% {
+    transform: scale(.4) translate(-28.57%);
+    opacity: .3;
+  }
+}

+ 1 - 1
src/contexts/themeContext.js

@@ -19,7 +19,7 @@ export const ThemeProvider = ({children}) => {
     const isMobile=()=>{
         return width<768;
     }
-    const [theme, setTheme] = useState(persisted && persisted.theme ?  persisted.theme : (browserTheme.matches ? 'light' : 'dark'));
+    const [theme, setTheme] = useState(persisted && persisted.theme ?  persisted.theme : ('dark'));
     // const [fontScale, setFontScale] = useState(persisted.fontScale || 1);
     const [fontScale, setFontScale] = useState(isMobile()?0.88:1);
     const [direction, setDirection] = useState(persisted.direction || (isRtl ? 'rtl' : 'ltr'));

+ 28 - 9
src/features/api.js

@@ -1,22 +1,26 @@
 import {createAsyncThunk} from "@reduxjs/toolkit";
-import {APP_PAGE_MODULES, APP_ROUTES, REQ_STATUS} from "@api/config";
+import {APP_GAME_LIST, APP_PAGE_MODULES, APP_ROUTES, REQ_STATUS} from "@api/config";
 import Http from "@api/request";
+import {updateGameList} from "@features/pageSlice";
 
-export const buildReqReducer=(builder,thunk,callback)=>{
+export const buildReqReducer=(builder,thunk,callback,statusCB=null)=>{
     if (builder==null||thunk==null)return;
     builder
-        .addCase(thunk.pending, (state) => {
+        .addCase(thunk.pending, (state,action) => {
             //正在请求
-            state.status=REQ_STATUS.LOADING;
+            // state.status=REQ_STATUS.LOADING;
+            if (statusCB)statusCB(state,REQ_STATUS.LOADING,action.meta.arg);
         })
         .addCase(thunk.fulfilled, (state, action) => {
             //请求成功
-            if (callback)callback(state,action);
-            state.status=REQ_STATUS.IDLE;
+            if (statusCB)statusCB(state,REQ_STATUS.IDLE,action.meta.arg);
+            if (callback)callback(state,action,action.meta.arg);
+            // state.status=REQ_STATUS.IDLE;
         })
-        .addCase(thunk.rejected, (state) => {
+        .addCase(thunk.rejected, (state,action) => {
             //请求失败
-            state.status=REQ_STATUS.FAILED;
+            // state.status=REQ_STATUS.FAILED;
+            if (statusCB)statusCB(state,REQ_STATUS.FAILED,action.meta.arg);
         });
 }
 //api
@@ -33,7 +37,22 @@ export const reqPageModules=createAsyncThunk(
     APP_PAGE_MODULES,
     async (pageID,thunkAPI) => {
         const response = await Http.get({url:APP_PAGE_MODULES+"/"+pageID});
-        if (response)response.pageID=pageID;
+        if (response){
+            response.pageID=pageID;
+        }
+        return response;
+    },
+)
+//gamelist more
+export const reqGameList=createAsyncThunk(
+    APP_GAME_LIST,
+    async (arg,thunkAPI) => {
+        const {mkey,tabtype,page}=arg;
+        // https://api.ouro777.com/game/gameList/?type=2&page=2
+        const url=APP_GAME_LIST+"?type="+tabtype+"&page="+page;
+        const response = await Http.get({url:url});
+        if (response)response.mkey=mkey;
+        thunkAPI.dispatch(updateGameList(response));
         return response;
     },
 )

+ 36 - 0
src/features/gameListSlice.js

@@ -0,0 +1,36 @@
+import {createSlice} from "@reduxjs/toolkit";
+import {REQ_STATUS} from "@api/config";
+import {buildReqReducer, reqGameList, reqPageModules} from "@features/api";
+
+const reducerGameList=(state,action)=>{
+    let json=action.payload;
+    if (json&&json.code===0){
+
+    }
+}
+const gameListStatus=(state,status,arg)=>{
+    if (arg){
+        const {mkey}=arg;
+        state.fetchStatus[mkey]=status;
+    }
+}
+
+export const GameList=createSlice({
+    name: 'gamelist',
+    initialState: {
+        fetchStatus:{}
+    },
+    reducers: {
+    },
+    extraReducers: (builder) => {
+        buildReqReducer(builder,reqGameList,reducerGameList,gameListStatus);
+    },
+})
+export const {} = GameList.actions;
+export const getGameListStatus=(mkey)=>(state)=>{
+    let fetchStatus=state.gamelist.fetchStatus;
+    let status=fetchStatus[mkey]==undefined?REQ_STATUS.NONE:fetchStatus[mkey];
+    return status;
+}
+
+export default GameList.reducer;

+ 24 - 4
src/features/mainSlice.js

@@ -23,13 +23,15 @@ const generateRoutes=(routes,asyncRoutes=[])=>{
     const inRoutes=[];
     const popRoutes=[];
     //路由等级
-    const _generate=(list,plevel)=>{
+    //ppath parent path
+    const _generate=(list,plevel,ppath)=>{
         Object.keys(list).map((key)=>{
             let route=list[key];
             if (route){
                 if (allMap[route.path]==null){
                     //level
                     route.plv=plevel;
+                    route.ppath=ppath;
                     route.lv=plevel+key+"_";
                     allMap[route.path]=route;
                     let type=route.type;
@@ -37,12 +39,12 @@ const generateRoutes=(routes,asyncRoutes=[])=>{
                     else if (type==ROUTE_TYPE_POP)popRoutes.push(route);
                 }
                 let subs=route.subs;
-                if (subs!=null)_generate(subs,route.lv);
+                if (subs!=null)_generate(subs,route.lv,route.path);
             }
         });
     }
     const allroutes=[...routes, ...asyncRoutes];
-    _generate(allroutes,"");
+    _generate(allroutes,"","");
     const gnavs=generateNavs(allroutes);
     return {allMap,inRoutes,popRoutes,blockNavs:gnavs.navs,allNavs:gnavs.allNavs};
 }
@@ -54,6 +56,9 @@ const reducerRoutes=(state,action)=>{
         Object.assign(state, {routes:routes,asyncRoutes:data,...generData});
     }
 }
+const routesStatus=(state,status,arg)=>{
+    state.status=status;
+}
 export const Main=createSlice({
     name: 'main',
     initialState: {
@@ -67,7 +72,7 @@ export const Main=createSlice({
     extraReducers: (builder) => {
         // Add reducers for additional action types here, and handle loading state as needed
         // reducerRoutes(builder);
-        buildReqReducer(builder,reqGetRoutes,reducerRoutes);
+        buildReqReducer(builder,reqGetRoutes,reducerRoutes,routesStatus);
     },
 })
 export const {} = Main.actions;
@@ -95,6 +100,21 @@ export const getRoute=(path)=>(state)=>{
     if (route==null)return {};
     return route;
 }
+export const getRouteChain=(path)=>(state)=>{
+    let map=routesMap(state);
+    let list=[];
+    let getChain=(cpath)=>{
+        let route=map[cpath];
+        if (route!=null){
+            list.unshift(route);
+            if (route.ppath!=null&&route.ppath!==""){
+                getChain(route.ppath);
+            }
+        }
+    }
+    getChain(path);
+    return list;
+}
 export const isFullRoute=(path)=>(state)=>{
     let route=routesMap(state)[path];
     if (route)return route.type===ROUTE_TYPE_FULL;

+ 58 - 5
src/features/pageSlice.js

@@ -1,14 +1,54 @@
-import {createSlice} from "@reduxjs/toolkit";
+import {createAction, createSlice} from "@reduxjs/toolkit";
 import {REQ_STATUS} from "@api/config";
 import {buildReqReducer, reqPageModules} from "@features/api";
-import {routesMap} from "@features/mainSlice";
+
+const parseModules=(data,key="")=>{
+    if (data!=null){
+        for (let i = 0; i <data.length ; i++) {
+            let item=data[i];
+            if (item!=null){
+                let tkey=key+"_"+i;
+                item["_mkey"]=tkey;
+                let tdata=item.data;
+                if (tdata!=null){
+                    tkey=tkey+"_data";
+                    item.data=parseModules(tdata,tkey);
+                }
+            }
+        }
+    }
+    return data;
+}
+//mkey module key
+const getModuleData=(data,mkey)=>{
+    let module=null;
+    if (data!=null){
+        let keys=mkey.split("_");
+        let tdata=data;
+        for (let i = 0; i < keys.length; i++) {
+            let key=keys[i];
+            let temp=tdata[key];
+            if (temp==null)break;
+            tdata=temp;
+        }
+        module=tdata;
+    }
+    return module;
+}
+//page modules
 const reducerPage=(state,action)=>{
     let json=action.payload;
     if (json&&json.code===0){
         let data=json.data==null?[]:json.data;
-        state.pageData[json.pageID]=data;
+        let pageID=json.pageID;
+        data=parseModules(data,pageID);
+        state.pageData[pageID]=data;
     }
 }
+const pageStatus=(state,status,arg)=>{
+    state.status=status;
+}
+//page modules
 export const Page=createSlice({
     name: 'page',
     initialState: {
@@ -16,12 +56,25 @@ export const Page=createSlice({
         pageData:{}
     },
     reducers: {
+        updateGameList:((state, action) => {
+            let payload=action.payload;
+            let mkey=payload.mkey;
+            let module=getModuleData(state.pageData,mkey);
+            if (module!=null){
+                let oldData=module.data;
+                let data=payload.data;
+                let newData=[];
+                if (oldData!=null&&data!=null)newData=oldData.concat(data);
+                let newObj={page:payload.page,totalPage:payload.totalPage,data:newData};
+                Object.assign(module,newObj);
+            }
+        })
     },
     extraReducers: (builder) => {
-        buildReqReducer(builder,reqPageModules,reducerPage);
+        buildReqReducer(builder,reqPageModules,reducerPage,pageStatus);
     },
 })
-export const {} = Page.actions;
+export const {updateGameList} = Page.actions;
 export const getFetchStatus=(state)=>{
     return state.page.status;
 }

+ 50 - 0
src/layout/BreadCrumb/index.jsx

@@ -0,0 +1,50 @@
+import styles from './styles.module.scss';
+import {useEffect} from "react";
+import {NavLink, useLocation} from "react-router-dom";
+import {useSelector} from "react-redux";
+import {getRouteChain} from "@features/mainSlice";
+import symbol from '@assets/symbol-defs.svg';
+const i18n=global.i18n;
+const BreadCrumb=()=>{
+    const {pathname} = useLocation();
+    const chain=useSelector(getRouteChain(pathname));
+    useEffect(() => {
+    }, [pathname]);
+    const renderBreadCrumb=()=>{
+        let len=chain.length;
+        if (len<=1)return (<></>);
+        return (
+            <div className={styles.container}>
+                <div className={styles.game_nav_path}>
+                    {
+                        chain.map((item,index)=>{
+                            let end=index===len-1;
+                            return (
+                                <>
+                                    <NavLink key={index} to={`${!end?item.path:''}`}  className={`${!end?styles.nav_hover:styles.nav_active}`}>
+                                        <span>{i18n.t(item.title)}</span>
+                                    </NavLink>
+                                    {
+                                        !end?(
+                                            <svg className={styles.icon} key={"arrow_"+index}>
+                                                <use xlinkHref={`${symbol}#icon_Arrow`} />
+                                            </svg>
+                                        ):(<></>)
+                                    }
+                                </>
+                            )
+                        })
+                    }
+                </div>
+            </div>
+        )
+    }
+    return (
+        <>
+            {
+                renderBreadCrumb()
+            }
+        </>
+    )
+}
+export default BreadCrumb;

+ 33 - 0
src/layout/BreadCrumb/styles.module.scss

@@ -0,0 +1,33 @@
+.container{
+  width: 100%;
+  & .game_nav_path{
+    background-color: var(--nav-item-hover);
+    padding: 0 .9375rem;
+    height: 2.5rem;
+    line-height: 2.5rem;
+    border-radius: var(--border-radius);
+    display: inline-block;
+    vertical-align: center;
+    white-space: nowrap;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    & .nav_hover{
+      color: var(--text);
+      &:hover{
+        color: var(--primary-color);
+      }
+    }
+    & .nav_active{
+      color: var(--header);
+      cursor: default;
+    }
+    & .icon{
+      width: .75rem;
+      height: .75rem;
+      display: inline-block;
+      vertical-align: center;
+      fill: var(--text);
+      margin: .8125rem .375rem 0;
+    }
+  }
+}

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

@@ -49,7 +49,8 @@
     "view_all": "View all"
   },
   "game_list": {
-    "show_more": "Load More"
+    "show_more": "Load More",
+    "no_more": "No More"
   },
   "game_search": {
     "input_ph": "Game name",

+ 9 - 1
src/widgets/ModuleGameList/index.jsx

@@ -1,12 +1,20 @@
 import React from "react";
 import GameList from "@components/GameList";
 import {useThemeProvider} from "@contexts/themeContext";
+import {useDispatch, useSelector} from "react-redux";
+import {getGameListStatus} from "@features/gameListSlice";
+import {reqGameList} from "@features/api";
 
 const ModuleGameList=({data})=>{
     const {isMobile} = useThemeProvider();
+    const dispatch = useDispatch();
+    const status=useSelector(getGameListStatus(data._mkey));
+    const reqMore=(tabtype,page)=>{
+        dispatch(reqGameList({mkey:data._mkey,tabtype,page}));
+    }
     return (
         <div className={``}>
-            <GameList data={data}>
+            <GameList data={data} status={status} reqMore={reqMore}>
 
             </GameList>
         </div>

+ 2 - 0
src/widgets/ModuleGroup/index.jsx

@@ -7,6 +7,7 @@ import {REQ_STATUS} from "@api/config";
 import LoadingScreen from "@components/LoadingScreen";
 import AllWidgets from "@widgets/index";
 import styles from "./styles.module.scss";
+import BreadCrumb from "@layout/BreadCrumb";
 
 const ModuleGroup=({pageID})=>{
     const dispatch = useDispatch();
@@ -22,6 +23,7 @@ const ModuleGroup=({pageID})=>{
             return (
                 <Suspense fallback={<LoadingScreen />}>
                     <div className={`${styles.container} ${isMobile()?'':'page-max-width-wrap'}`}>
+                        <BreadCrumb />
                         {
                             modules.map((item,index)=>{
                                 let Comp=AllWidgets[item.type];