Appearance
React18-爱彼迎
项目搭建-项目基本配置和目录结构划分
创建项目
- create-react-app airbnb(创建完成之后删掉不需要的文件)
项目配置
- 配置项目的icon(自己配)
- 配置项目的标题(自己配)
- 配置jsconfig.json(用于智能提示,会好很多)
json
{
"compilerOptions": {
"target": "es5",
"module": "esnext",
"baseUrl": "./",
"moduleResolution": "node",
"paths": {
"@/*": ["src/*"]
},
"jsx": "preserve",
"lib": ["esnext", "dom", "dom.iterable", "scripthost"]
}
}
项目目录结构划分
项目搭建-craco配置别名和less样式
通过craco配置别名和less文件:
- 因为react把配置文件都隐藏起来了
安装craco
- npm i @craco/craco@alpha -D
项目根目录创建craco.config.js文件
javascript
const path = require("path");
const resolve = (pathname) => path.resolve(__dirname, pathname);
module.exports = {
webpack: {
alias: {
"@": resolve("src"),
"components": resolve("src/components"),
"utils": resolve("src/utils"),
},
},
};
配置less
- npm i craco-less@2.1.0-alpha.0 -D
javascript
const path = require("path");
const CracoLessPlugin = require('craco-less');
const resolve = (pathname) => path.resolve(__dirname, pathname);
module.exports = {
// webpack
webpack: {
alias: {
"@": resolve("src"),
"components": resolve("src/components"),
"utils": resolve("src/utils"),
},
},
// less
plugins: [
{
plugin: CracoLessPlugin,
},
],
};
项目搭建-css的样式重置
安装重置样式文件
- npm i normalize.css
- 在index.js中导入normalize.css
- 在assets/css中新建reset.less,用于重置我们自己的样式
- 在assets/css中新建variables.less,用于存储一些less变量
- 在assets/css中新建index.less,用于导入reset.less,并在index.js中导入index.less
css
@textColor: #484848;
@textColorSecondary: #222;
css
@import "./variables.less";
body,button,form,h1,h2,h3,h4,h5,h6,input,li,p,td,textarea,th,ul {
padding: 0;
margin: 0;
}
a {
color: @textColor;
text-decoration: none;
}
img {
vertical-align: top;
}
ul,
li {
list-style: none;
}
css
@import "./reset.less";
jsx
import React from "react";
import ReactDOM from "react-dom/client";
import App from "@/App";
import 'normalize.css'
import '@/assets/css/index.less'
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
项目搭建-项目路由Router的搭建和配置
安装路由
- npm i react-router-dom
在index.js中导入路由组件
jsx
import React from "react";
import ReactDOM from "react-dom/client";
import { HashRouter } from "react-router-dom";
import App from "@/App";
import "normalize.css";
import "@/assets/css/index.less";
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<React.StrictMode>
<HashRouter>
<App />
</HashRouter>
</React.StrictMode>
);
在view中创建三个页面
- home/index.jsx
- detail/index.jsx
- entire/index.jsx
jsx
import React, { memo } from 'react'
const Home = memo(() => {
return (
<div>Home</div>
)
})
export default Home
jsx
import React, { memo } from 'react'
const Detail = memo(() => {
return (
<div>Detail</div>
)
})
export default Detail
jsx
import React, { memo } from 'react'
const Entire = memo(() => {
return (
<div>Entire</div>
)
})
export default Entire
在router中创建index.js编写路由映射表
- Navigate用于路由的重定向,当这个组件出现时,就会执行跳转到对应的to路径中
javascript
import React from "react";
import { Navigate } from "react-router-dom";
const Home = React.lazy(() => import("@/views/home"));
const Entire = React.lazy(() => import("@/views/entire"));
const Detail = React.lazy(() => import("@/views/detail"));
const routes = [
{
path: "/",
element: <Navigate to="/home" />,
},
{
path: "/home",
element: <Home />,
},
{
path: "/entire",
element: <Entire />,
},
{
path: "/detail",
element: <Detail />,
},
];
export default routes;
编写app.jsx
jsx
import React, { memo } from "react";
import { useRoutes } from "react-router-dom";
import routes from "./router";
const App = memo(() => {
return (
<div className="app">
<div className="header">header</div>
<div className="page">{useRoutes(routes)}</div>
<div className="footer">footer</div>
</div>
);
});
export default App;
index.js中导入路由组件进行包裹,因为用到了组件懒加载,所以需要从react中导入Suspense组件进行包裹
jsx
import React, { Suspense } from "react";
import ReactDOM from "react-dom/client";
import { HashRouter } from "react-router-dom";
import App from "@/App";
import "normalize.css";
import "@/assets/css/index.less";
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<React.StrictMode>
<Suspense fallback="loading">
<HashRouter>
<App />
</HashRouter>
</Suspense>
</React.StrictMode>
);
项目搭建-项目状态管理redux的配置
Redux状态管理的选择
- 普通方式:目前项目中依然使用率非常高
- @reduxjs/toolkit方式:推荐方式,未来的趋势
- 这两种该项目中都会用到
安装@reduxjs/toolkit工具和react-redux
- 因为@reduxjs/toolkit已经集成了redux了,所以不需要再安装redux
- npm i @reduxjs/toolkit react-redux
在store中新建modules用于编写redux模块
- 在modules里面新建home.js(该模块采用RTK模式)
javascript
import { createSlice } from "@reduxjs/toolkit";
const homeSlice = createSlice({
name: "home",
initialState: {},
reducers: {},
});
export default homeSlice.reducer;
- 在modules里新建entire文件夹(该模块采用原始模式)
- 在里面新建createActions.js
- 在里面新建constants.js(放置常量)
- 在里面新建reducer.js
javascript
const initialState = {};
function reducer(state = initialState, action) {
switch (action.type) {
default:
return state;
}
}
export default reducer;
- 在里面新建index.js,导入reducer并导出
javascript
import reducer from "./reducer";
export default reducer;
在store中新建index.js编写redux相关代码
javascript
import { configureStore } from "@reduxjs/toolkit";
import homeReducer from "./modules/home";
import entireReducer from "./modules/entire";
const store = configureStore({
reducer: {
home: homeReducer,
entire: entireReducer,
},
});
export default store;
在index.js中导入store
jsx
import React, { Suspense } from "react";
import ReactDOM from "react-dom/client";
import { HashRouter } from "react-router-dom";
import { Provider } from "react-redux";
import store from "@/store";
import App from "@/App";
import "normalize.css";
import "@/assets/css/index.less";
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<React.StrictMode>
<Suspense fallback="loading">
<Provider store={store}>
<HashRouter>
<App />
</HashRouter>
</Provider>
</Suspense>
</React.StrictMode>
);
项目搭建-网络请求axios的封装和测试
安装axios
- npm i axios
在services中新建request文件夹
- 在里面新建index.js,在里面封装一个类,通过这个类对axios进行二次封装
javascript
import axios from "axios";
import { BASE_URL, TIMEOUT } from "./config";
class CJRequest {
constructor(baseURL, timeout) {
this.instance = axios.create({
baseURL,
timeout,
});
this.instance.interceptors.response.use(
(res) => {return res.data;},
(err) => {return err;}
);
}
request(config) {
return this.instance.request(config);
}
get(config) {
return this.request({ ...config, method: "get" });
}
post(config) {
return this.request({ ...config, method: "post" });
}
}
export default new CJRequest(BASE_URL, TIMEOUT);
- 在里面新建一个config.js文件,用于编写一些配置信息
javascript
export const BASE_URL = "http://codercba.com:1888/airbnb/api";
export const TIMEOUT = 10000;
在services中新建modules文件夹,每个模块都有自己独立的文件来管理网络请求
在services中创建index.js文件作为service统一出口
javascript
import cjRequest from "./request";
export default cjRequest;
在home页面中测试网络请求
javascript
import React, { memo, useEffect, useState } from "react";
import cjRequest from "@/services";
const Home = memo(() => {
// 定义状态
const [highScore, setHighScore] = useState({});
// 网络请求的代码
useEffect(() => {
cjRequest.get({ url: "/home/highscore" }).then((res) => {
console.log(res);
setHighScore(res);
});
}, []);
return (
<div>
<h2>{highScore.title}</h2>
<h4>{highScore.subtitle}</h4>
<ul>
{highScore.list?.map((item) => {
return <li key={item.id}>{item.name}</li>;
})}
</ul>
</div>
);
});
export default Home;
项目-整体App架构和Header不同实现方案
制作头部有两种方案
- 一种是放在APP中共用一个头部
- 一种是三个页面都分别引入头部
- 这里采用第一种较为复杂的做法
分析把header封装成一个组件
- 在components中新建app-header文件夹和app-footer文件夹
- 在app-header中新建index.jsx
jsx
import React, { memo } from "react";
const AppHeader = memo(() => {
return <div>AppHeader</div>;
});
export default AppHeader;
- 在app-footer中新建index.jsx
jsx
import React, { memo } from 'react'
const AppFooter = memo(() => {
return (
<div>AppFooter</div>
)
})
export default AppFooter
- 在app.jsx中导入app-header和app-footer组件
jsx
import React, { memo } from "react";
import { useRoutes } from "react-router-dom";
import routes from "./router";
import AppHeader from "./components/app-header";
import AppFooter from "./components/app-footer";
const App = memo(() => {
return (
<div className="app">
<AppHeader />
<div className="page">{useRoutes(routes)}</div>
<AppFooter />
</div>
);
});
export default App;
项目-Header的封装和整体布局
安装样式工具库和插件
- npm i styled-components
创建header组件样式文件并导出用于包裹的样式组件
- 在components/app-header中新建用于编写样式文件,导出样式组件
- 在components/app-header/index.jsx中导入样式组件进行
AppHeader组件比较复杂,需要分成左中右三块
- 在app-header中新建文件夹,用于存放AppHeader的子组件
- 在c-cpns中新建,header-left,header-center,header-right
- 在三个文件夹里面index.jsx和style.js
- 在AppHeader组件中导入三个子组件
- 编写AppHeader组件样式
总体代码如下
jsx
import styled from "styled-components";
export const LeftWrapper = styled.div`
flex: 1;
`;
jsx
import React, { memo } from "react";
import { LeftWrapper } from "./style";
const HeaderLeft = memo(() => {
return <LeftWrapper>HeaderLeft</LeftWrapper>;
});
export default HeaderLeft;
jsx
import styled from "styled-components";
export const CenterWrapper = styled.div`
`;
jsx
import React, { memo } from "react";
import { CenterWrapper } from "./style";
const HeaderCenter = memo(() => {
return <CenterWrapper>HeaderCenter</CenterWrapper>;
});
export default HeaderCenter;
jsx
import React, { memo } from "react";
import { RightWrapper } from "./style";
const HeaderRight = memo(() => {
return <RightWrapper>HeaderRight</RightWrapper>;
});
export default HeaderRight;
jsx
import styled from "styled-components";
export const RightWrapper = styled.div`
flex: 1;
display: flex;
justify-content: flex-end;
`;
jsx
import styled from "styled-components";
export const HeaderWrapper = styled.div`
display: flex;
align-items: center;
height: 80px;
border-bottom: 1px solid #eee;
`;
jsx
import React, { memo } from "react";
import HeaderLeft from "./c-cpns/header-left";
import HeaderCenter from "./c-cpns/header-center";
import HeaderRight from "./c-cpns/header-right";
import { HeaderWrapper } from "./style";
const AppHeader = memo(() => {
return (
<HeaderWrapper>
<HeaderLeft />
<HeaderCenter />
<HeaderRight />
</HeaderWrapper>
);
});
export default AppHeader;
项目-Logo的svg图片的两种使用方式
svg有两种使用方式
- 一种是直接使用svg标签
- 另一种是使用svg图片
在assets中新建svg文件夹,把svg封装成一个组件,再导入到HeaderLeft中
封装一个工具函数styleStrToObject把style字符串转换成对象(因为jsx不支持style属性为字符串)
- 在assets/svg中创建一个utils文件夹,里面新建一个index.js,编写styleStrToObject函数
这样做的好处是svg一大段代码不会干扰到正常编写HeaderLeft组件
javascript
function styleStrToObject(styleStr) {
const obj = {};
const s = styleStr
.toLowerCase()
.replace(/-(.)/g, function (m, g) {
return g.toUpperCase();
})
.replace(/;\s?$/g, "")
.split(/:|;/g);
for (var i = 0; i < s.length; i += 2) {
obj[s[i].replace(/\s/g, "")] = s[i + 1].replace(/^\s+|\s+$/g, "");
}
return obj;
}
export default styleStrToObject;
jsx
import React, { memo } from "react";
import { LeftWrapper } from "./style";
import IconLogo from "@/assets/svg/icon_logo";
const HeaderLeft = memo(() => {
return (
<LeftWrapper>
<IconLogo />
</LeftWrapper>
);
});
export default HeaderLeft;
在header-left/style.js中可以修改svg颜色
jsx
import styled from "styled-components";
export const LeftWrapper = styled.div`
flex: 1;
color: red;
`;
项目-Logo颜色配置和主题文件配置
在assets文件夹中新建theme,在里面新建index.js编写主题颜色
javascript
const Theme = {
color: {
primaryColor: "#ff385c",
secondaryColor: "#00848A",
},
};
export default Theme;
在index.js中,从style-components中导入ThemeProvider组件用于提供主题
jsx
import React, { Suspense } from "react";
import ReactDOM from "react-dom/client";
import { HashRouter } from "react-router-dom";
import { Provider } from "react-redux";
import { ThemeProvider } from "styled-components";
import theme from "./assets/theme";
import store from "@/store";
import App from "@/App";
import "normalize.css";
import "@/assets/css/index.less";
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<React.StrictMode>
<Suspense fallback="loading">
<Provider store={store}>
<ThemeProvider theme={theme}>
<HashRouter>
<App />
</HashRouter>
</ThemeProvider>
</Provider>
</Suspense>
</React.StrictMode>
);
使用主题颜色
jsx
import styled from "styled-components";
export const LeftWrapper = styled.div`
flex: 1;
color: red;
display: flex;
align-items: center;
color: ${(props) => props.theme.color.primaryColor};
.logo {
margin-left: 24px;
cursor: pointer;
}
`;
项目-Header的右侧布局和内容展示
布局header右侧
- 官网获取三个图标,方式按照之前的方式,制作成组件
- 新建主题颜色
jsx
const Theme = {
color: {
primaryColor: "#ff385c",
secondaryColor: "#00848A",
},
text: {
primaryColor: "#484848",
secondaryColor: "#222",
},
};
export default Theme;
- 编写布局
jsx
import React, { memo } from "react";
import { RightWrapper } from "./style";
import IconGlobal from "@/assets/svg/icon_global";
import IconMenu from "@/assets/svg/icon_menu";
import IconAvatar from "@/assets/svg/icon_avatar";
const HeaderRight = memo(() => {
return (
<RightWrapper>
<div className="btns">
<span className="btn">登录</span>
<span className="btn">注册</span>
<span className="btn">
<IconGlobal />
</span>
</div>
<div className="profile">
<IconMenu />
<IconAvatar />
</div>
</RightWrapper>
);
});
export default HeaderRight;
- 编写样式
jsx
import styled from "styled-components";
export const RightWrapper = styled.div`
flex: 1;
display: flex;
justify-content: flex-end;
align-items: center;
color: ${(props) => props.theme.text.primaryColor};
font-size: 14px;
font-weight: 600;
.btns {
display: flex;
.btn {
height: 18px;
line-height: 18px;
padding: 12px 15px;
cursor: pointer;
border-radius: 22px;
&:hover {
background-color: #f5f5f5;
}
}
}
.profile {
display: flex;
justify-content: space-evenly;
align-items: center;
width: 77px;
height: 42px;
margin-right: 24px;
box-sizing: border-box;
border: 1px solid #ccc;
border-radius: 25px;
background-color: #fff;
color: #999;
cursor: pointer;
}
`;
项目-box-shadow的动画效果和样式混入
编写通用hover,用混入的特性,其他地方也可以用到
jsx
const Theme = {
color: {
primaryColor: "#ff385c",
secondaryColor: "#00848A",
},
text: {
primaryColor: "#484848",
secondaryColor: "#222",
},
mixin:{
boxShadow:`
transition: box-shadow 200ms ease;
&:hover {
box-shadow: 0 2px 4px rgba(0,0,0,.18);
}
`
}
};
export default Theme;
HeaderRight组件中使用
项目-Header的中间搜索框布局和展示
抽取搜索图标svg
编写HeaderCenter结构和样式代码
jsx
import React, { memo } from "react";
import { CenterWrapper } from "./style";
import IconSearchBar from "@/assets/svg/icon_search";
const HeaderCenter = memo(() => {
return (
<CenterWrapper>
<div className="search-bar">
<div className="text">搜索房源和体验</div>
<div className="icon">
<IconSearchBar />
</div>
</div>
</CenterWrapper>
);
});
export default HeaderCenter;
jsx
import styled from "styled-components";
export const CenterWrapper = styled.div`
.search-bar {
display: flex;
justify-content: space-between;
align-items: center;
width: 300px;
height: 48px;
box-sizing: border-box;
padding: 0 8px;
border: 1px solid #ddd;
border-radius: 24px;
cursor: pointer;
${(props) => props.theme.mixin.boxShadow};
.text {
padding: 0 16px;
color: #222;
font-weight: 600;
}
.icon {
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
border-radius: 50%;
color: #fff;
background-color: ${(props) => props.theme.color.primaryColor};
}
}
`;
jsx
body {
font-size: 14px;
}
jsx
@import "./reset.less";
@import "./common.less";
项目-Profile点击面板切换的效果
编写Profile点击面板
编写结构和样式,绑定点击事件
jsx
import React, { memo, useEffect } from "react";
import { RightWrapper } from "./style";
import IconGlobal from "@/assets/svg/icon_global";
import IconMenu from "@/assets/svg/icon_menu";
import IconAvatar from "@/assets/svg/icon_avatar";
import { useState } from "react";
const HeaderRight = memo(() => {
// 定义组件内部的状态
const [showPanel, setShowPanel] = useState(false);
// 副作用代码
useEffect(() => {
function windowHandleClick() {
setShowPanel(false);
}
window.addEventListener("click", windowHandleClick, true);
return () => {
window.removeEventListener("click", windowHandleClick, true);
};
}, []);
// 事件处理函数
function profileClickHandle() {
setShowPanel(true);
}
return (
<RightWrapper>
<div className="btns">
<span className="btn">登录</span>
<span className="btn">注册</span>
<span className="btn">
<IconGlobal />
</span>
</div>
<div className="profile" onClick={profileClickHandle}>
<IconMenu />
<IconAvatar />
{showPanel && (
<div className="panel">
<div className="top">
<div className="item register">注册</div>
<div className="item login">登录</div>
</div>
<div className="bottom">
<div className="item">出租房源</div>
<div className="item">开展体验</div>
<div className="item">帮助</div>
</div>
</div>
)}
</div>
</RightWrapper>
);
});
export default HeaderRight;
jsx
import styled from "styled-components";
export const RightWrapper = styled.div`
flex: 1;
display: flex;
justify-content: flex-end;
align-items: center;
color: ${(props) => props.theme.text.primaryColor};
font-size: 14px;
font-weight: 600;
.btns {
display: flex;
.btn {
height: 18px;
line-height: 18px;
padding: 12px 15px;
cursor: pointer;
border-radius: 22px;
&:hover {
background-color: #f5f5f5;
}
}
}
.profile {
position: relative;
display: flex;
justify-content: space-evenly;
align-items: center;
width: 77px;
height: 42px;
margin-right: 24px;
box-sizing: border-box;
border: 1px solid #ccc;
border-radius: 25px;
background-color: #fff;
color: #999;
cursor: pointer;
${(props) => props.theme.mixin.boxShadow}
.panel {
position: absolute;
top: 54px;
right: 0;
width: 240px;
background-color: #fff;
border-radius: 10px;
box-shadow: 0 0 6px rgba(0, 0, 0, 0.2);
color: #666;
.top,
.bottom {
padding: 10px 0;
}
.item {
height: 40px;
line-height: 40px;
padding: 0 16px;
&:hover {
background-color: #f5f5f5;
}
}
.top {
border-bottom: 1px solid #eee;
}
}
}
`;
项目-首页-顶部轮播图图片的展示
在views/home文件夹中新建c-cpns,用于放置home的子组件
- 在c-cpns里面新建home-banner文件夹,编写轮播图图片
jsx
import React, { memo } from "react";
import { BannerWrapper } from "./style";
const HomeBanner = memo(() => {
return <BannerWrapper>HomeBanner</BannerWrapper>;
});
export default HomeBanner;
jsx
import styled from "styled-components";
// import coverImg from "@/assets/img/cover_01.jpg";
export const BannerWrapper = styled.div`
height: 529px;
background: url(${require("@/assets/img/cover_01.jpg")}) center/cover;
`;
在Home组件中导入HomeBanner组件
jsx
import React, { memo } from "react";
import HomeBanner from "./c-cpns/home-banner";
import { HomeWrapper } from "./style";
const Home = memo(() => {
return (
<HomeWrapper>
<HomeBanner />
</HomeWrapper>
);
});
export default Home;
jsx
import styled from "styled-components";
export const HomeWrapper = styled.div`
`
项目-高性价比数据Redux获取和管理
在redux中请求数据,并展示到home组件中
- 对每个模块的请求数据操作进行独立封装,在services/modules中新建home.js
jsx
import cjRequest from "..";
export function getHomeGoodPriceData() {
return cjRequest.get({
url: "/home/goodprice",
});
}
- 编写store/modules/home.js这个模块
- 导入用于执行异步(请求数据)操作
- 创建异步操作的action:,并导出
- 在中监听成功时,修改store数据goodPriceInfo
- 高亮的部分是同步修改的action(可不写)
jsx
import { getHomeGoodPriceData } from "@/services";
import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
export const fetchHomeDataAction = createAsyncThunk("fetchdata", async () => {
const res = await getHomeGoodPriceData();
return res;
});
const homeSlice = createSlice({
name: "home",
initialState: {
goodPriceInfo: {},
},
reducers: {
changeGoodPriceInfoAction(state, { payload }) {
state.goodPriceInfo = payload;
},
},
extraReducers: {
[fetchHomeDataAction.fulfilled](state, { payload }) {
state.goodPriceInfo = payload;
},
},
});
export const { changeGoodPriceInfoAction } = homeSlice.actions;
export default homeSlice.reducer;
- home组件中发起派发异步action:,获取数据并展示
- **注意使用可选链防止一开始没有goodPriceInfo.list数据,循环渲染报错 **
jsx
import { fetchHomeDataAction } from "@/store/modules/home";
import React, { memo, useEffect } from "react";
import { shallowEqual, useDispatch, useSelector } from "react-redux";
import HomeBanner from "./c-cpns/home-banner";
import { HomeWrapper } from "./style";
const Home = memo(() => {
// 从redux中获取数据
const { goodPriceInfo } = useSelector(
(state) => ({
goodPriceInfo: state.home.goodPriceInfo,
}),
shallowEqual
);
// 派发异步的事件:发送网络请求
const dispatch = useDispatch();
useEffect(() => {
dispatch(fetchHomeDataAction());
}, [dispatch]);
return (
<HomeWrapper>
<HomeBanner />
<div className="content">
<h2>{goodPriceInfo.title}</h2>
{goodPriceInfo.list?.map((item) => {
return <li key={item.id}>{item.name}</li>;
})}
</div>
</HomeWrapper>
);
});
export default Home;
项目-首页-区域的header封装过程
把房子列表展示封装成组件
先把头部抽成组件
- 这个组件其他地方可能也会用到,就放到通用组件里面
- 在components中创建section-header文件夹存在该组件
jsx
import PropTypes from "prop-types";
import React, { memo } from "react";
import { HeaderWrapper } from "./style";
const SectionHeader = memo((props) => {
const { title, subtitle } = props;
return (
<HeaderWrapper>
<h2 className="title">{title}</h2>
{subtitle && <div className="subtitle">{subtitle}</div>}
</HeaderWrapper>
);
});
SectionHeader.propTypes = {
title: PropTypes.string,
subtitle: PropTypes.string,
};
export default SectionHeader;
jsx
import styled from "styled-components";
export const HeaderWrapper = styled.div`
color: #222;
.title {
font-size: 22px;
font-weight: 700;
margin-bottom: 16px;
}
.subtitle {
font-size: 16px;
margin-bottom: 20px;
}
`;
在Home组件中使用该头部组件
jsx
import { fetchHomeDataAction } from "@/store/modules/home";
import React, { memo, useEffect } from "react";
import { shallowEqual, useDispatch, useSelector } from "react-redux";
import HomeBanner from "./c-cpns/home-banner";
import SectionHeader from "@/components/section-header";
import { HomeWrapper } from "./style";
const Home = memo(() => {
// 从redux中获取数据
const { goodPriceInfo } = useSelector(
(state) => ({
goodPriceInfo: state.home.goodPriceInfo,
}),
shallowEqual
);
// 派发异步的事件:发送网络请求
const dispatch = useDispatch();
useEffect(() => {
dispatch(fetchHomeDataAction());
}, [dispatch]);
return (
<HomeWrapper>
<HomeBanner />
<div className="content">
<div className="good-price">
<SectionHeader title={goodPriceInfo.title} />
<ul>
{goodPriceInfo.list?.map((item) => {
return <li key={item.id}>{item.name}</li>;
})}
</ul>
</div>
</div>
</HomeWrapper>
);
});
export default Home;
jsx
import styled from "styled-components";
export const HomeWrapper = styled.div`
> .content {
width: 1032px;
margin: 0 auto;
}
.good-price {
margin-top: 30px;
}
`;
项目-首页-房间的item封装和整体布局
foot制作,代码略
把item封装成组件
- 在components中新建room-item文件夹,存放该组件,创建index.jsx和style.js
jsx
import PropTypes from "prop-types";
import React, { memo } from "react";
import { ItemWrapper } from "./style";
const RoomItem = memo((props) => {
const { itemData } = props;
return (
<ItemWrapper>
<div>{itemData.name}</div>
</ItemWrapper>
);
});
RoomItem.propTypes = {
itemData: PropTypes.object,
};
export default RoomItem;
jsx
import styled from "styled-components";
export const ItemWrapper = styled.div`
box-sizing: border-box;
width: 25%;
padding: 8px;
`;
在Home组件中导入RoomItem组件
jsx
import { fetchHomeDataAction } from "@/store/modules/home";
import React, { memo, useEffect } from "react";
import { shallowEqual, useDispatch, useSelector } from "react-redux";
import HomeBanner from "./c-cpns/home-banner";
import SectionHeader from "@/components/section-header";
import RoomItem from "@/components/room-item";
import { HomeWrapper } from "./style";
const Home = memo(() => {
// 从redux中获取数据
const { goodPriceInfo } = useSelector(
(state) => ({
goodPriceInfo: state.home.goodPriceInfo,
}),
shallowEqual
);
// 派发异步的事件:发送网络请求
const dispatch = useDispatch();
useEffect(() => {
dispatch(fetchHomeDataAction());
}, [dispatch]);
return (
<HomeWrapper>
<HomeBanner />
<div className="content">
<div className="good-price">
<SectionHeader title={goodPriceInfo.title} />
<ul className="room-list">
{goodPriceInfo.list?.slice(0, 8).map((item) => {
return <RoomItem itemData={item} key={item.id} />;
})}
</ul>
</div>
</div>
</HomeWrapper>
);
});
export default Home;
jsx
import styled from "styled-components";
export const HomeWrapper = styled.div`
> .content {
width: 1032px;
margin: 0 auto;
}
.good-price {
margin-top: 30px;
.room-list {
display: flex;
flex-wrap: wrap;
margin: 0 -8px;
}
}
`;
项目-首页-房间item的布局的展示过程
完善RoomItem组件
jsx
import PropTypes from "prop-types";
import React, { memo } from "react";
import { ItemWrapper } from "./style";
const RoomItem = memo((props) => {
const { itemData } = props;
console.log(itemData);
return (
<ItemWrapper verifyColor={itemData?.verify_info?.text_color || "#39576a"}>
<div className="inner">
<div className="cover">
<img src={itemData.picture_url} alt="" />
</div>
<div className="desc">{itemData.verify_info.messages.join(" · ")}</div>
<div className="name">{itemData.name}</div>
<div className="price">¥{itemData.price}/晚</div>
</div>
</ItemWrapper>
);
});
RoomItem.propTypes = {
itemData: PropTypes.object,
};
export default RoomItem;
jsx
import styled from "styled-components";
export const ItemWrapper = styled.div`
box-sizing: border-box;
width: 25%;
padding: 8px;
.inner {
width: 100%;
}
.cover {
position: relative;
box-sizing: border-box;
padding: 66.66% 8px 0;
border-radius: 3px;
overflow: hidden;
img {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
}
}
.desc {
margin: 10px 0 5px;
font-size: 12px;
font-weight: 700;
color: ${(props) => props.verifyColor};
}
.name {
font-size: 16px;
font-weight: 700;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.price {
margin: 8px 0;
}
`;
jsx
body {
font-size: 14px;
font-family: "Circular", "PingFang-SC", "Hiragino Sans GB", "微软雅黑", "Microsoft YaHei", "Heiti SC";
color: #484848;
}
项目-首页-房间的item底部内容的展示
项目集成material ui和antd ui,步骤略
使用material ui的打分器,用于房间item底部评分展示
jsx
import PropTypes from "prop-types";
import React, { memo } from "react";
import { ItemWrapper } from "./style";
import Rating from "@mui/material/Rating";
const RoomItem = memo((props) => {
const { itemData } = props;
console.log(itemData);
return (
<ItemWrapper verifyColor={itemData?.verify_info?.text_color || "#39576a"}>
<div className="inner">
<div className="cover">
<img src={itemData.picture_url} alt="" />
</div>
<div className="desc">{itemData.verify_info.messages.join(" · ")}</div>
<div className="name">{itemData.name}</div>
<div className="price">¥{itemData.price}/晚</div>
<div className="bottom">
<Rating
value={itemData.star_rating ?? 5}
precision={0.1}
readOnly
sx={{ fontSize: "12px", color: "#008489" }}
/>
<span className="count">{itemData.reviews_count}</span>
{
itemData?.bottom_info && <span className="extra">· {itemData.bottom_info.content}</span>
}
</div>
</div>
</ItemWrapper>
);
});
RoomItem.propTypes = {
itemData: PropTypes.object,
};
export default RoomItem;
jsx
import styled from "styled-components";
export const ItemWrapper = styled.div`
box-sizing: border-box;
width: 25%;
padding: 8px;
.inner {
width: 100%;
}
.cover {
position: relative;
box-sizing: border-box;
padding: 66.66% 8px 0;
border-radius: 3px;
overflow: hidden;
img {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
}
}
.desc {
margin: 10px 0 5px;
font-size: 12px;
font-weight: 700;
color: ${(props) => props.verifyColor};
}
.name {
font-size: 16px;
font-weight: 700;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.price {
margin: 8px 0;
}
.bottom {
display: flex;
align-items: center;
font-size: 12px;
font-weight: 600;
color: ${(props) => props.theme.text.primaryColor};
.count {
margin: 0 2px 0 4px;
}
.MuiRating-icon {
margin-right: -2px;
}
}
`;
项目-首页-房间列表的组件代码重构
把整个房间ul列表封装成组件
- 在components中创建一个section-rooms组件文件夹,把列表展示抽取过去
jsx
import PropTypes from "prop-types";
import React, { memo } from "react";
import RoomItem from "@/components/room-item";
import { RoomsWrap } from "./style";
const SectionRooms = memo((props) => {
const { roomList = [] } = props;
return (
<RoomsWrap>
{roomList.slice(0, 8).map((item) => {
return <RoomItem itemData={item} key={item.id} />;
})}
</RoomsWrap>
);
});
SectionRooms.propTypes = {
roomList: PropTypes.array,
};
export default SectionRooms;
jsx
import styled from "styled-components";
export const RoomsWrap = styled.div`
display: flex;
flex-wrap: wrap;
margin: 0 -8px;
`;
home组件修改
jsx
import { fetchHomeDataAction } from "@/store/modules/home";
import React, { memo, useEffect } from "react";
import { shallowEqual, useDispatch, useSelector } from "react-redux";
import HomeBanner from "./c-cpns/home-banner";
import SectionHeader from "@/components/section-header";
import SectionRooms from "@/components/section-rooms";
import { HomeWrapper } from "./style";
const Home = memo(() => {
// 从redux中获取数据
const { goodPriceInfo } = useSelector(
(state) => ({
goodPriceInfo: state.home.goodPriceInfo,
}),
shallowEqual
);
// 派发异步的事件:发送网络请求
const dispatch = useDispatch();
useEffect(() => {
dispatch(fetchHomeDataAction());
}, [dispatch]);
return (
<HomeWrapper>
<HomeBanner />
<div className="content">
<div className="good-price">
<SectionHeader title={goodPriceInfo.title} />
<SectionRooms roomList={goodPriceInfo.list} />
</div>
</div>
</HomeWrapper>
);
});
export default Home;
把原来ul的样式抽走
jsx
import styled from "styled-components";
export const HomeWrapper = styled.div`
> .content {
width: 1032px;
margin: 0 auto;
}
.good-price {
margin-top: 30px;
}
`;
项目-首页-高评分数据的获取和展示过程
在services/modules/home.js模块中封装请求高分好评房源列表数据
jsx
import cjRequest from "..";
export function getHomeGoodPriceData() {
return cjRequest.get({
url: "/home/goodprice",
});
}
export function getHomeHighScoreData() {
return cjRequest.get({
url: "/home/highscore",
});
}
在store/modules/home.js中编写redux相关代码
- 把使用promise把reducer换成同步的写法,不用额外的reducer了
jsx
import { getHomeGoodPriceData, getHomeHighScoreData } from "@/services";
import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
export const fetchHomeDataAction = createAsyncThunk(
"fetchdata",
async (payload, { dispatch }) => {
getHomeGoodPriceData().then((res) => {
dispatch(changeGoodPriceInfoAction(res));
});
getHomeHighScoreData().then((res) => {
dispatch(changeHightScoreInfoAction(res));
});
}
);
const homeSlice = createSlice({
name: "home",
initialState: {
goodPriceInfo: {},
highScoreInfo: {},
},
reducers: {
changeGoodPriceInfoAction(state, { payload }) {
state.goodPriceInfo = payload;
},
changeHightScoreInfoAction(state, { payload }) {
state.highScoreInfo = payload;
},
}
});
export const {
changeGoodPriceInfoAction,
changeHightScoreInfoAction
} = homeSlice.actions;
export default homeSlice.reducer;
首页展示数据,抽取整个组件
- 在view/home/c-cpns中创建一个文件夹home-section-v1用于存放该组件
jsx
import SectionHeader from "@/components/section-header";
import SectionRooms from "@/components/section-rooms";
import PropTypes from "prop-types";
import React, { memo } from "react";
import { SectionV1Wrapper } from "./style";
const index = memo((props) => {
const { infoData } = props;
return (
<SectionV1Wrapper>
<SectionHeader title={infoData.title} subtitle={infoData.subtitle} />
<SectionRooms roomList={infoData.list} />
</SectionV1Wrapper>
);
});
index.propTypes = {
infoData: PropTypes.object,
};
export default index;
jsx
import styled from "styled-components";
export const SectionV1Wrapper = styled.div`
margin-top: 30px;
`;
修改Home组件
jsx
import { fetchHomeDataAction } from "@/store/modules/home";
import React, { memo, useEffect } from "react";
import { shallowEqual, useDispatch, useSelector } from "react-redux";
import HomeBanner from "./c-cpns/home-banner";
import HomeSectionV1 from "./c-cpns/home-section-v1";
import { HomeWrapper } from "./style";
const Home = memo(() => {
// 从redux中获取数据
const { goodPriceInfo, highScoreInfo } = useSelector(
(state) => ({
goodPriceInfo: state.home.goodPriceInfo,
highScoreInfo: state.home.highScoreInfo,
}),
shallowEqual
);
// 派发异步的事件:发送网络请求
const dispatch = useDispatch();
useEffect(() => {
dispatch(fetchHomeDataAction());
}, [dispatch]);
return (
<HomeWrapper>
<HomeBanner />
<div className="content">
<HomeSectionV1 infoData={goodPriceInfo} />
<HomeSectionV1 infoData={highScoreInfo} />
</div>
</HomeWrapper>
);
});
export default Home;
jsx
import styled from "styled-components";
export const HomeWrapper = styled.div`
> .content {
width: 1032px;
margin: 0 auto;
}
`;
项目-首页-折扣数据的分析和获取管理
在services/modules/home.js模块中继续封装请求方法
jsx
import cjRequest from "..";
export function getHomeGoodPriceData() {
return cjRequest.get({
url: "/home/goodprice",
});
}
export function getHomeHighScoreData() {
return cjRequest.get({
url: "/home/highscore",
});
}
export function getHomeDiscountData() {
return cjRequest.get({
url: "/home/discount",
});
}
store中编写redux相关代码
jsx
import {
getHomeDiscountData,
getHomeGoodPriceData,
getHomeHighScoreData,
} from "@/services";
import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
export const fetchHomeDataAction = createAsyncThunk(
"fetchdata",
async (payload, { dispatch }) => {
getHomeGoodPriceData().then((res) => {
dispatch(changeGoodPriceInfoAction(res));
});
getHomeHighScoreData().then((res) => {
dispatch(changeHightScoreInfoAction(res));
});
getHomeDiscountData().then((res) => {
dispatch(changeDiscountInfoAction(res));
});
}
);
const homeSlice = createSlice({
name: "home",
initialState: {
goodPriceInfo: {},
highScoreInfo: {},
discountInfo: {},
},
reducers: {
changeGoodPriceInfoAction(state, { payload }) {
state.goodPriceInfo = payload;
},
changeHightScoreInfoAction(state, { payload }) {
state.highScoreInfo = payload;
},
changeDiscountInfoAction(state, { payload }) {
state.discountInfo = payload;
},
},
});
export const {
changeGoodPriceInfoAction,
changeHightScoreInfoAction,
changeDiscountInfoAction,
} = homeSlice.actions;
export default homeSlice.reducer;
项目-首页-折扣数据的展示和item动态宽度
更改item的width,让折扣数据可以一行显示3个
jsx
import PropTypes from "prop-types";
import React, { memo } from "react";
import { ItemWrapper } from "./style";
import Rating from "@mui/material/Rating";
const RoomItem = memo((props) => {
const { itemData, itemWidth = "25%" } = props;
return (
<ItemWrapper
verifyColor={itemData?.verify_info?.text_color || "#39576a"}
itemWidth={itemWidth}
>
{/* ............. */}
</ItemWrapper>
);
});
RoomItem.propTypes = {
itemData: PropTypes.object,
itemWidth: PropTypes.string,
};
export default RoomItem;
jsx
import styled from "styled-components";
export const ItemWrapper = styled.div`
box-sizing: border-box;
/* width: 25%; */
width: ${(props) => props.itemWidth};
padding: 8px;
.inner {
width: 100%;
}
/* ............ */
`;
在首页中展示折扣数据(暂时写死)
jsx
import { fetchHomeDataAction } from "@/store/modules/home";
import React, { memo, useEffect } from "react";
import { shallowEqual, useDispatch, useSelector } from "react-redux";
import HomeBanner from "./c-cpns/home-banner";
import HomeSectionV1 from "./c-cpns/home-section-v1";
import SectionHeader from "@/components/section-header";
import SectionRooms from "@/components/section-rooms";
import { HomeWrapper } from "./style";
const Home = memo(() => {
// 从redux中获取数据
const { goodPriceInfo, highScoreInfo, discountInfo } = useSelector(
(state) => ({
goodPriceInfo: state.home.goodPriceInfo,
highScoreInfo: state.home.highScoreInfo,
discountInfo: state.home.discountInfo,
}),
shallowEqual
);
// 派发异步的事件:发送网络请求
const dispatch = useDispatch();
useEffect(() => {
dispatch(fetchHomeDataAction());
}, [dispatch]);
return (
<HomeWrapper>
<HomeBanner />
<div className="content">
<div className="discount">
<SectionHeader
title={discountInfo.title}
subtitle={discountInfo.subtitle}
/>
<SectionRooms
roomList={discountInfo.dest_list?.["成都"]}
itemWidth="33.33%"
/>
</div>
<HomeSectionV1 infoData={goodPriceInfo} />
<HomeSectionV1 infoData={highScoreInfo} />
</div>
</HomeWrapper>
);
});
export default Home;
更新section-rooms和home-section-v1
jsx
import PropTypes from "prop-types";
import React, { memo } from "react";
import RoomItem from "@/components/room-item";
import { RoomsWrap } from "./style";
const SectionRooms = memo((props) => {
const { roomList = [], itemWidth } = props;
return (
<RoomsWrap>
{roomList.slice(0, 8).map((item) => {
return <RoomItem itemData={item} key={item.id} itemWidth={itemWidth} />;
})}
</RoomsWrap>
);
});
SectionRooms.propTypes = {
roomList: PropTypes.array,
itemWidth: PropTypes.string,
};
export default SectionRooms;
jsx
import SectionHeader from "@/components/section-header";
import SectionRooms from "@/components/section-rooms";
import PropTypes from "prop-types";
import React, { memo } from "react";
import { SectionV1Wrapper } from "./style";
const index = memo((props) => {
const { infoData } = props;
return (
<SectionV1Wrapper>
<SectionHeader title={infoData.title} subtitle={infoData.subtitle} />
<SectionRooms roomList={infoData.list} itemWidth="25%" />
</SectionV1Wrapper>
);
});
index.propTypes = {
infoData: PropTypes.object,
};
export default index;
项目-首页-折扣区域tabs的封装和切换
封装选项卡组件
在components中新建section-tabs文件夹,在里面新建index.jsx和style.js
- 安装classnames,npm i classnames
jsx
import PropTypes from "prop-types";
import React, { memo, useState } from "react";
import classNames from "classnames";
import { TabsWrapper } from "./style";
const SectionTabs = memo((props) => {
const { tabNames = [] } = props;
const [currentIndex, setCurrentIndex] = useState(0);
function itemClickHandle(index) {
setCurrentIndex(index);
}
return (
<TabsWrapper>
{tabNames.map((item, index) => {
return (
<div
className={classNames("item", { active: currentIndex === index })}
key={index}
onClick={(e) => itemClickHandle(index)}
>
{item}
</div>
);
})}
</TabsWrapper>
);
});
SectionTabs.propTypes = {
tabNames: PropTypes.array,
};
export default SectionTabs;
jsx
import styled from "styled-components";
export const TabsWrapper = styled.div`
display: flex;
.item {
box-sizing: border-box;
flex-basis: 120px;
flex-shrink: 0;
padding: 14px 16px;
margin-right: 16px;
border-radius: 3px;
font-size: 17px;
text-align: center;
border: 0.5px solid #d8d8d8;
white-space: nowrap;
cursor: pointer;
${(props) => props.theme.mixin.boxShadow}
&:last-child {
margin-right: 0;
}
&.active {
color: #fff;
background-color: ${(props) => props.theme.color.secondaryColor};
}
}
`;
Home组件导入SectionTabs组件
jsx
import { fetchHomeDataAction } from "@/store/modules/home";
import React, { memo, useEffect, useState } from "react";
import { shallowEqual, useDispatch, useSelector } from "react-redux";
import HomeBanner from "./c-cpns/home-banner";
import HomeSectionV1 from "./c-cpns/home-section-v1";
import SectionHeader from "@/components/section-header";
import SectionRooms from "@/components/section-rooms";
import SectionTabs from "@/components/section-tabs";
import { HomeWrapper } from "./style";
const Home = memo(() => {
// 从redux中获取数据
const { goodPriceInfo, highScoreInfo, discountInfo } = useSelector(
(state) => ({
goodPriceInfo: state.home.goodPriceInfo,
highScoreInfo: state.home.highScoreInfo,
discountInfo: state.home.discountInfo,
}),
shallowEqual
);
// 数据的转换
const tabNames = discountInfo.dest_address?.map((item) => item.name);
// 派发异步的事件:发送网络请求
const dispatch = useDispatch();
useEffect(() => {
dispatch(fetchHomeDataAction());
}, [dispatch]);
return (
<HomeWrapper>
<HomeBanner />
<div className="content">
<div className="discount">
<SectionHeader
title={discountInfo.title}
subtitle={discountInfo.subtitle}
/>
<SectionTabs tabNames={tabNames} />
<SectionRooms
roomList={discountInfo.dest_list?.["成都"]}
itemWidth="33.33%"
/>
</div>
<HomeSectionV1 infoData={goodPriceInfo} />
<HomeSectionV1 infoData={highScoreInfo} />
</div>
</HomeWrapper>
);
});
export default Home;
项目-首页-tabs的name切换-数据切换
把城市的点击事件传递出去,实现切换数据效果
- 函数传递给子组件时,可以用useCallback做性能优化
jsx
import PropTypes from "prop-types";
import React, { memo, useState } from "react";
import classNames from "classnames";
import { TabsWrapper } from "./style";
const SectionTabs = memo((props) => {
const { tabNames = [], tabClick } = props;
const [currentIndex, setCurrentIndex] = useState(0);
function itemClickHandle(index, item) {
setCurrentIndex(index);
tabClick(index, item);
}
return (
<TabsWrapper>
{tabNames.map((item, index) => {
return (
<div
className={classNames("item", { active: currentIndex === index })}
key={index}
onClick={(e) => itemClickHandle(index, item)}
>
{item}
</div>
);
})}
</TabsWrapper>
);
});
SectionTabs.propTypes = {
tabNames: PropTypes.array,
};
export default SectionTabs;
jsx
import { fetchHomeDataAction } from "@/store/modules/home";
import React, { memo, useEffect, useCallback, useState } from "react";
import { shallowEqual, useDispatch, useSelector } from "react-redux";
import HomeBanner from "./c-cpns/home-banner";
import HomeSectionV1 from "./c-cpns/home-section-v1";
import SectionHeader from "@/components/section-header";
import SectionRooms from "@/components/section-rooms";
import SectionTabs from "@/components/section-tabs";
import { HomeWrapper } from "./style";
const Home = memo(() => {
// 从redux中获取数据
const { goodPriceInfo, highScoreInfo, discountInfo } = useSelector(
(state) => ({
goodPriceInfo: state.home.goodPriceInfo,
highScoreInfo: state.home.highScoreInfo,
discountInfo: state.home.discountInfo,
}),
shallowEqual
);
// 数据的转换
const tabNames = discountInfo.dest_address?.map((item) => item.name);
const [name, setName] = useState("佛山");
const tabClickHandle = useCallback(function (index, name) {
setName(name);
}, []);
// 派发异步的事件:发送网络请求
const dispatch = useDispatch();
useEffect(() => {
dispatch(fetchHomeDataAction());
}, [dispatch]);
return (
<HomeWrapper>
<HomeBanner />
<div className="content">
<div className="discount">
<SectionHeader
title={discountInfo.title}
subtitle={discountInfo.subtitle}
/>
<SectionTabs tabNames={tabNames} tabClick={tabClickHandle} />
<SectionRooms
roomList={discountInfo.dest_list?.[name]}
itemWidth="33.33%"
/>
</div>
<HomeSectionV1 infoData={goodPriceInfo} />
<HomeSectionV1 infoData={highScoreInfo} />
</div>
</HomeWrapper>
);
});
export default Home;
项目-首页-区域v2的封装和初次渲染的数据
在home/c-cpns中创建home-section-v2文件夹,抽取组件
处理默认name的问题(默认第一列展示佛山,但是不能写死,要从数据里面获取)
- 两种解决方案
- 判断discountInfo有值时才进行渲染
- 封装一个工具函数isEmptyObject用于判断是否是空对象
- useEffect进行监听,当infoData改变时重新设置name,但是会导致组件渲染三次
- 第一次是默认组件渲染
- 第二次是数据改变渲染
- 第三次是主动设置渲染
- 判断discountInfo有值时才进行渲染
jsx
import styled from "styled-components";
export const SectionV2Wrapper = styled.div`
margin-top: 30px;
`;
jsx
import PropTypes from "prop-types";
import React, { memo, useState, useCallback } from "react";
import SectionHeader from "@/components/section-header";
import SectionRooms from "@/components/section-rooms";
import SectionTabs from "@/components/section-tabs";
import { SectionV2Wrapper } from "./style";
// import { useEffect } from "react";
const HomeSectionV2 = memo((props) => {
// 从props获取数据
const { infoData } = props;
const initialName = Object.keys(infoData.dest_list)[0];
// 定义内部的state
const [name, setName] = useState(initialName);
// 数据的转换
const tabNames = infoData.dest_address?.map((item) => item.name);
// useEffect(() => {
// setName(initialName);
// }, [initialName]);
// 事件处理函数
const tabClickHandle = useCallback(function (index, name) {
console.log(index, name);
setName(name);
}, []);
return (
<SectionV2Wrapper>
<SectionHeader title={infoData.title} subtitle={infoData.subtitle} />
<SectionTabs tabNames={tabNames} tabClick={tabClickHandle} />
<SectionRooms roomList={infoData.dest_list?.[name]} itemWidth="33.33%" />
</SectionV2Wrapper>
);
});
HomeSectionV2.propTypes = {
infoData: PropTypes.object,
};
export default HomeSectionV2;
jsx
export function isEmptyO(obj) {
return !!Object.keys(obj).length;
}
jsx
export * from "./is-empty-object";
jsx
import { fetchHomeDataAction } from "@/store/modules/home";
import React, { memo, useEffect } from "react";
import { shallowEqual, useDispatch, useSelector } from "react-redux";
import HomeBanner from "./c-cpns/home-banner";
import HomeSectionV1 from "./c-cpns/home-section-v1";
import HomeSectionV2 from "./c-cpns/home-section-v2";
import { isEmptyO } from "@/utils";
import { HomeWrapper } from "./style";
const Home = memo(() => {
// 从redux中获取数据
const { goodPriceInfo, highScoreInfo, discountInfo } = useSelector(
(state) => ({
goodPriceInfo: state.home.goodPriceInfo,
highScoreInfo: state.home.highScoreInfo,
discountInfo: state.home.discountInfo,
}),
shallowEqual
);
// 派发异步的事件:发送网络请求
const dispatch = useDispatch();
useEffect(() => {
dispatch(fetchHomeDataAction());
}, [dispatch]);
return (
<HomeWrapper>
<HomeBanner />
<div className="content">
{isEmptyO(discountInfo) && <HomeSectionV2 infoData={discountInfo} />}
{isEmptyO(goodPriceInfo) && <HomeSectionV1 infoData={goodPriceInfo} />}
{isEmptyO(highScoreInfo) && <HomeSectionV1 infoData={highScoreInfo} />}
</div>
</HomeWrapper>
);
});
export default Home;
项目-首页-推荐数据的请求和展示过程
在services/modules/home.js模块中继续封装请求推荐数据的方法
jsx
import cjRequest from "..";
export function getHomeGoodPriceData() {
return cjRequest.get({
url: "/home/goodprice",
});
}
export function getHomeHighScoreData() {
return cjRequest.get({
url: "/home/highscore",
});
}
export function getHomeDiscountData() {
return cjRequest.get({
url: "/home/discount",
});
}
export function getHomeHotRecommendData() {
return cjRequest.get({
url: "/home/hotrecommenddest",
});
}
编写redux操作代码
jsx
import {
getHomeDiscountData,
getHomeGoodPriceData,
getHomeHighScoreData,
getHomeHotRecommendData,
} from "@/services";
import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
export const fetchHomeDataAction = createAsyncThunk(
"fetchdata",
async (payload, { dispatch }) => {
getHomeGoodPriceData().then((res) => {
dispatch(changeGoodPriceInfoAction(res));
});
getHomeHighScoreData().then((res) => {
dispatch(changeHightScoreInfoAction(res));
});
getHomeDiscountData().then((res) => {
dispatch(changeDiscountInfoAction(res));
});
getHomeHotRecommendData().then((res) => {
dispatch(changeRecommendInfoAction(res));
});
}
);
const homeSlice = createSlice({
name: "home",
initialState: {
goodPriceInfo: {},
highScoreInfo: {},
discountInfo: {},
recommendInfo: {},
},
reducers: {
changeGoodPriceInfoAction(state, { payload }) {
state.goodPriceInfo = payload;
},
changeHightScoreInfoAction(state, { payload }) {
state.highScoreInfo = payload;
},
changeDiscountInfoAction(state, { payload }) {
state.discountInfo = payload;
},
changeHotRecommendInfoAction(state, { payload }) {
state.recommendInfo = payload;
},
},
});
export const {
changeGoodPriceInfoAction,
changeHightScoreInfoAction,
changeDiscountInfoAction,
changeRecommendInfoAction,
} = homeSlice.actions;
export default homeSlice.reducer;
Home组件展示数据
jsx
import { fetchHomeDataAction } from "@/store/modules/home";
import React, { memo, useEffect } from "react";
import { shallowEqual, useDispatch, useSelector } from "react-redux";
import HomeBanner from "./c-cpns/home-banner";
import HomeSectionV1 from "./c-cpns/home-section-v1";
import HomeSectionV2 from "./c-cpns/home-section-v2";
import { isEmptyO } from "@/utils";
import { HomeWrapper } from "./style";
const Home = memo(() => {
// 从redux中获取数据
const { goodPriceInfo, highScoreInfo, discountInfo, recommendInfo } =
useSelector(
(state) => ({
goodPriceInfo: state.home.goodPriceInfo,
highScoreInfo: state.home.highScoreInfo,
discountInfo: state.home.discountInfo,
recommendInfo: state.home.recommendInfo,
}),
shallowEqual
);
// 派发异步的事件:发送网络请求
const dispatch = useDispatch();
useEffect(() => {
dispatch(fetchHomeDataAction());
}, [dispatch]);
return (
<HomeWrapper>
<HomeBanner />
<div className="content">
{isEmptyO(discountInfo) && <HomeSectionV2 infoData={discountInfo} />}
{isEmptyO(recommendInfo) && <HomeSectionV2 infoData={recommendInfo} />}
{isEmptyO(goodPriceInfo) && <HomeSectionV1 infoData={goodPriceInfo} />}
{isEmptyO(highScoreInfo) && <HomeSectionV1 infoData={highScoreInfo} />}
</div>
</HomeWrapper>
);
});
export default Home;
项目-首页-区域Footer的封装和展示过程
把查看更多封装成独立组件
在components中新建section-footer文件夹,存放该组件
- 抽取箭头svg
- 样式通过传递一个name属性来判断是 显示全部 还是 查看更多xxx房源
jsx
import styled from "styled-components";
export const FooterWrapper = styled.div`
display: flex;
margin-top: 10px;
.info {
display: flex;
align-items: center;
cursor: pointer;
font-size: 17px;
color: ${(props) => props.color};
font-weight: 700;
&:hover {
text-decoration: underline;
}
.text {
margin-right: 6px;
}
}
`;
jsx
import PropTypes from "prop-types";
import React, { memo } from "react";
import { FooterWrapper } from "./style";
import IconMoreArrow from "@/assets/svg/icon_more_arrow";
const index = memo((props) => {
const { name = "" } = props;
let showMessage = "显示全部";
if (name) {
showMessage = `查看更多${name}房源`;
}
return (
<FooterWrapper color={name ? "#00848A" : "#000"}>
<div className="info">
<span>{showMessage}</span>
<IconMoreArrow />
</div>
</FooterWrapper>
);
});
index.propTypes = {
name: PropTypes.string,
};
export default index;
HomeSectionV1组件中导入SectionFooter组件
jsx
import SectionHeader from "@/components/section-header";
import SectionRooms from "@/components/section-rooms";
import SectionFooter from "@/components/section-footer";
import PropTypes from "prop-types";
import React, { memo } from "react";
import { SectionV1Wrapper } from "./style";
const index = memo((props) => {
const { infoData } = props;
return (
<SectionV1Wrapper>
<SectionHeader title={infoData.title} subtitle={infoData.subtitle} />
<SectionRooms roomList={infoData.list} itemWidth="25%" />
<SectionFooter />
</SectionV1Wrapper>
);
});
index.propTypes = {
infoData: PropTypes.object,
};
export default index;
HomeSectionV2组件中导入SectionFooter组件
jsx
import PropTypes from "prop-types";
import React, { memo, useState, useCallback } from "react";
import SectionHeader from "@/components/section-header";
import SectionRooms from "@/components/section-rooms";
import SectionTabs from "@/components/section-tabs";
import SectionFooter from "@/components/section-footer";
import { SectionV2Wrapper } from "./style";
// import { useEffect } from "react";
const HomeSectionV2 = memo((props) => {
// 从props获取数据
const { infoData } = props;
const initialName = Object.keys(infoData.dest_list)[0];
// 定义内部的state
const [name, setName] = useState(initialName);
// 数据的转换
const tabNames = infoData.dest_address?.map((item) => item.name);
// useEffect(() => {
// setName(initialName);
// }, [initialName]);
// 事件处理函数
const tabClickHandle = useCallback(function (index, name) {
setName(name);
}, []);
return (
<SectionV2Wrapper>
<SectionHeader title={infoData.title} subtitle={infoData.subtitle} />
<SectionTabs tabNames={tabNames} tabClick={tabClickHandle} />
<SectionRooms roomList={infoData.dest_list?.[name]} itemWidth="33.33%" />
<SectionFooter name={name} />
</SectionV2Wrapper>
);
});
HomeSectionV2.propTypes = {
infoData: PropTypes.object,
};
export default HomeSectionV2;
项目-首页-ScrollView组件的封装
封装组件
- 左侧滚动按钮
- 右侧滚动按钮
- 实现滚动功能,展示的内容由用户决定
在base-ui文件夹下创建scroll-view文件夹,存放该组件,可以在其他项目中使用
思路分析
- 使用插槽
- 当内容超出才显示右侧侧按钮
- 用一个变量记录按钮是否显示
- 可滚动宽度大于占据的宽度时右侧按钮显示
jsx
import styled from "styled-components";
export const ViewWrapper = styled.div`
overflow: hidden;
.scroll-content {
display: flex;
}
`;
jsx
import React, { memo, useState, useEffect, useRef } from "react";
import { ViewWrapper } from "./style";
const ScrollView = memo((props) => {
// 定义内部的状态
const [showRight, setShowRight] = useState(false);
const scrollContentRef = useRef();
// 组件渲染完毕,判断是否显示右侧的按钮
useEffect(() => {
// 一共可以滚动的宽度
const scrollWidth = scrollContentRef.current.scrollWidth;
// 本身占据的宽度
const clientWidth = scrollContentRef.current.clientWidth;
// 一共可以滚动的距离
const totalDistance = scrollWidth - clientWidth;
setShowRight(totalDistance > 0);
}, [props.children]);
return (
<ViewWrapper>
<button>左边按钮</button>
{showRight && <button>右边按钮</button>}
<div className="scroll-content" ref={scrollContentRef}>
{props.children}
</div>
</ViewWrapper>
);
});
ScrollView.propTypes = {};
export default ScrollView;
jsx
import PropTypes from "prop-types";
import React, { memo, useState } from "react";
import classNames from "classnames";
import { TabsWrapper } from "./style";
import ScrollView from "@/base-ui/scroll-view";
const SectionTabs = memo((props) => {
const { tabNames = [], tabClick } = props;
const [currentIndex, setCurrentIndex] = useState(0);
function itemClickHandle(index, item) {
setCurrentIndex(index);
tabClick(index, item);
}
return (
<TabsWrapper>
<ScrollView>
{tabNames.map((item, index) => {
return (
<div
className={classNames("item", { active: currentIndex === index })}
key={index}
onClick={(e) => itemClickHandle(index, item)}
>
{item}
</div>
);
})}
</ScrollView>
</TabsWrapper>
);
});
SectionTabs.propTypes = {
tabNames: PropTypes.array,
};
export default SectionTabs;
项目-首页-ScrollView点击右边按钮向左滚动
思路:计算滚动区间,有两种计算方法
- 第一种:前x个itemWidth + margin-right,缺点是越到后面越不好计算
- 第二种:直接拿到要滚动到的那个元素的offsetLeft偏移量
- 先拿到要滚动的是第几个item,用一个变量posIndex存储
- 每次点击右边按钮加1
- 其次再拿到具体的那个元素设置transform进行滚动
- 每次滚动完要判断是否继续显示右边按钮
- 已经滚动距离>可滚动距离就把右边按钮隐藏
- 设置position:relative才能获取正确的offsetLeft
jsx
import styled from "styled-components";
export const ViewWrapper = styled.div`
overflow: hidden;
.scroll-content {
position: relative;
display: flex;
transition: transform 250ms ease;
}
`;
使用useRef来存储对应的数据,有好几个性能优化的点
jsx
import React, { memo, useState, useEffect, useRef } from "react";
import { ViewWrapper } from "./style";
const ScrollView = memo((props) => {
// 定义内部的状态
const [showRight, setShowRight] = useState(false);
const [posIndex, setPosIndex] = useState(0);
const totalDistanceRef = useRef();
const scrollContentRef = useRef();
// 组件渲染完毕,判断是否显示右侧的按钮
useEffect(() => {
// 一共可以滚动的宽度
const scrollWidth = scrollContentRef.current.scrollWidth;
// 本身占据的宽度
const clientWidth = scrollContentRef.current.clientWidth;
// 一共可以滚动的距离
const totalDistance = scrollWidth - clientWidth;
totalDistanceRef.current = totalDistance;
setShowRight(totalDistance > 0);
}, [props.children]);
// 事件处理逻辑
function rightClickHandle() {
const newIndex = posIndex + 1;
const newEl = scrollContentRef.current.children[newIndex];
const newOffsetLeft = newEl.offsetLeft;
scrollContentRef.current.style.transform = `translateX(${-newOffsetLeft}px)`;
setPosIndex(newIndex);
// 是否继续显示右侧的按钮
setShowRight(totalDistanceRef.current > newOffsetLeft);
}
return (
<ViewWrapper>
<button>左边按钮</button>
{showRight && <button onClick={rightClickHandle}>右边按钮</button>}
<div className="scroll-content" ref={scrollContentRef}>
{props.children}
</div>
</ViewWrapper>
);
});
ScrollView.propTypes = {};
export default ScrollView;
项目-首页-ScrollView左侧按钮和按钮点击实现
实现左侧按钮点击效果,两个按钮的点击事件封装成一个函数
将按钮改成图标(左右箭头图标自行获取封装成组件)
jsx
import styled from "styled-components";
export const ViewWrapper = styled.div`
position: relative;
padding: 8px 0;
.scroll {
overflow: hidden;
.scroll-content {
display: flex;
transition: transform 250ms ease;
}
}
.control {
position: absolute;
z-index: 9;
display: flex;
justify-content: center;
align-items: center;
width: 28px;
height: 28px;
border-radius: 50%;
text-align: center;
border-width: 2px;
border-style: solid;
border-color: #fff;
background: #fff;
box-shadow: 0px 1px 1px 1px rgba(0, 0, 0, 0.14);
cursor: pointer;
&.left {
left: 0;
top: 50%;
transform: translate(-50%, -50%);
}
&.right {
right: 0;
top: 50%;
transform: translate(50%, -50%);
}
}
`;
jsx
import React, { memo, useState, useEffect, useRef } from "react";
import { ViewWrapper } from "./style";
import IconArrowLeft from "@/assets/svg/icon_arrow_left";
import IconArrowRight from "@/assets/svg/icon_arrow_right";
const ScrollView = memo((props) => {
// 定义内部的状态
const [showLeft, setShowLeft] = useState(false);
const [showRight, setShowRight] = useState(false);
const [posIndex, setPosIndex] = useState(0);
const totalDistanceRef = useRef();
const scrollContentRef = useRef();
// 组件渲染完毕,判断是否显示右侧的按钮
useEffect(() => {
// 一共可以滚动的宽度
const scrollWidth = scrollContentRef.current.scrollWidth;
// 本身占据的宽度
const clientWidth = scrollContentRef.current.clientWidth;
// 一共可以滚动的距离
const totalDistance = scrollWidth - clientWidth;
totalDistanceRef.current = totalDistance;
setShowRight(totalDistance > 0);
}, [props.children]);
// 事件处理逻辑
function controlClickHandle(num) {
const newIndex = posIndex + num;
const newEl = scrollContentRef.current.children[newIndex];
const newOffsetLeft = newEl.offsetLeft;
scrollContentRef.current.style.transform = `translateX(${-newOffsetLeft}px)`;
setPosIndex(newIndex);
// 是否继续显示右侧的按钮
setShowRight(totalDistanceRef.current > newOffsetLeft);
setShowLeft(newOffsetLeft > 0);
}
return (
<ViewWrapper>
{showLeft && (
<div className="control left" onClick={() => controlClickHandle(-1)}>
<IconArrowLeft />
</div>
)}
{showRight && (
<div className="control right" onClick={() => controlClickHandle(1)}>
<IconArrowRight />
</div>
)}
<div className="scroll">
<div className="scroll-content" ref={scrollContentRef}>
{props.children}
</div>
</div>
</ViewWrapper>
);
});
ScrollView.propTypes = {};
export default ScrollView;
项目-首页-向往数据的请求和滚动展示
services/modules/home.js模块封装请求向往数据方法
jsx
import cjRequest from "..";
export function getHomeGoodPriceData() {
return cjRequest.get({
url: "/home/goodprice",
});
}
export function getHomeHighScoreData() {
return cjRequest.get({
url: "/home/highscore",
});
}
export function getHomeDiscountData() {
return cjRequest.get({
url: "/home/discount",
});
}
export function getHomeHotRecommendData() {
return cjRequest.get({
url: "/home/hotrecommenddest",
});
}
export function getHomeLongforData() {
return cjRequest.get({
url: "/home/longfor",
});
}
编写redux相关代码
jsx
import {
getHomeDiscountData,
getHomeGoodPriceData,
getHomeHighScoreData,
getHomeHotRecommendData,
getHomeLongforData,
} from "@/services";
import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
export const fetchHomeDataAction = createAsyncThunk(
"fetchdata",
async (payload, { dispatch }) => {
getHomeGoodPriceData().then((res) => {
dispatch(changeGoodPriceInfoAction(res));
});
getHomeHighScoreData().then((res) => {
dispatch(changeHightScoreInfoAction(res));
});
getHomeDiscountData().then((res) => {
dispatch(changeDiscountInfoAction(res));
});
getHomeHotRecommendData().then((res) => {
dispatch(changeRecommendInfoAction(res));
});
getHomeLongforData().then((res) => {
dispatch(changeLongforInfoAction(res));
});
}
);
const homeSlice = createSlice({
name: "home",
initialState: {
goodPriceInfo: {},
highScoreInfo: {},
discountInfo: {},
recommendInfo: {},
longforInfo: {},
},
reducers: {
changeGoodPriceInfoAction(state, { payload }) {
state.goodPriceInfo = payload;
},
changeHightScoreInfoAction(state, { payload }) {
state.highScoreInfo = payload;
},
changeDiscountInfoAction(state, { payload }) {
state.discountInfo = payload;
},
changeRecommendInfoAction(state, { payload }) {
state.recommendInfo = payload;
},
changeLongforInfoAction(state, { payload }) {
state.longforInfo = payload;
},
},
});
export const {
changeGoodPriceInfoAction,
changeHightScoreInfoAction,
changeDiscountInfoAction,
changeRecommendInfoAction,
changeLongforInfoAction,
} = homeSlice.actions;
export default homeSlice.reducer;
把单个城市item封装成组件,在components中创建longfor-item文件夹存放该组件
jsx
import styled from "styled-components";
export const ItemWrapper = styled.div`
flex-shrink: 0;
width: 20%;
.inner {
padding: 8px;
.item-info {
position: relative;
border-radius: 3px;
overflow: hidden;
}
}
.cover {
width: 100%;
}
.bg-cover {
position: absolute;
left: 0;
right: 0;
bottom: 0;
height: 60%;
background-image: linear-gradient(
-180deg,
rgba(0, 0, 0, 0) 3%,
rgb(0, 0, 0) 100%
);
}
.info {
position: absolute;
left: 8px;
right: 8px;
bottom: 0;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding: 0 24px 32px;
color: #fff;
.city {
font-size: 18px;
font-weight: 600;
}
.price {
font-size: 14px;
margin-top: 5px;
}
}
`;
jsx
import PropTypes from "prop-types";
import React, { memo } from "react";
import { ItemWrapper } from "./style";
const LongforItem = memo((props) => {
const { itemData } = props;
return (
<ItemWrapper>
<div className="inner">
<div className="item-info">
<img className="cover" src={itemData.picture_url} alt="" />
<div className="bg-cover"></div>
<div className="info">
<div className="city">{itemData.city}</div>
<div className="price">均价:{itemData.price}</div>
</div>
</div>
</div>
</ItemWrapper>
);
});
LongforItem.propTypes = {
itemData: PropTypes.object,
};
export default LongforItem;
longfor区域封装成一个组件,在views/home/c-cpns中新建home-longfor文件夹
jsx
import styled from "styled-components";
export const LongforWrapper = styled.div`
margin-top: 30px;
.longfor-list {
display: flex;
margin: 0 -8px;
}
`;
jsx
import PropTypes from "prop-types";
import React, { memo } from "react";
import { LongforWrapper } from "./style";
import SectionHeader from "@/components/section-header";
import ScrollView from "@/base-ui/scroll-view";
import LongforItem from "@/components/longfor-item";
const HomeLongfor = memo((props) => {
const { infoData } = props;
return (
<LongforWrapper>
<SectionHeader title={infoData.title} subtitle={infoData.subtitle} />
<div className="longfor-list">
<ScrollView>
{infoData.list.map((item) => {
return <LongforItem itemData={item} key={item.city} />;
})}
</ScrollView>
</div>
</LongforWrapper>
);
});
HomeLongfor.propTypes = {
infoData: PropTypes.object,
};
export default HomeLongfor;
在Home组件中进行展示
jsx
import { fetchHomeDataAction } from "@/store/modules/home";
import React, { memo, useEffect } from "react";
import { shallowEqual, useDispatch, useSelector } from "react-redux";
import HomeBanner from "./c-cpns/home-banner";
import HomeSectionV1 from "./c-cpns/home-section-v1";
import HomeSectionV2 from "./c-cpns/home-section-v2";
import HomeLongfor from "./c-cpns/home-longfor";
import { isEmptyO } from "@/utils";
import { HomeWrapper } from "./style";
const Home = memo(() => {
// 从redux中获取数据
const {
goodPriceInfo,
highScoreInfo,
discountInfo,
recommendInfo,
longforInfo,
} = useSelector(
(state) => ({
goodPriceInfo: state.home.goodPriceInfo,
highScoreInfo: state.home.highScoreInfo,
discountInfo: state.home.discountInfo,
recommendInfo: state.home.recommendInfo,
longforInfo: state.home.longforInfo,
}),
shallowEqual
);
// 派发异步的事件:发送网络请求
const dispatch = useDispatch();
useEffect(() => {
dispatch(fetchHomeDataAction());
}, [dispatch]);
return (
<HomeWrapper>
<HomeBanner />
<div className="content">
{isEmptyO(discountInfo) && <HomeSectionV2 infoData={discountInfo} />}
{isEmptyO(recommendInfo) && <HomeSectionV2 infoData={recommendInfo} />}
{isEmptyO(longforInfo) && <HomeLongfor infoData={longforInfo} />}
{isEmptyO(goodPriceInfo) && <HomeSectionV1 infoData={goodPriceInfo} />}
{isEmptyO(highScoreInfo) && <HomeSectionV1 infoData={highScoreInfo} />}
</div>
</HomeWrapper>
);
});
export default Home;
项目-首页-plus数据的请求和展示过程
services/modules/home.js模块封装请求plus数据方法
jsx
import cjRequest from "..";
export function getHomeGoodPriceData() {
return cjRequest.get({
url: "/home/goodprice",
});
}
export function getHomeHighScoreData() {
return cjRequest.get({
url: "/home/highscore",
});
}
export function getHomeDiscountData() {
return cjRequest.get({
url: "/home/discount",
});
}
export function getHomeHotRecommendData() {
return cjRequest.get({
url: "/home/hotrecommenddest",
});
}
export function getHomeLongforData() {
return cjRequest.get({
url: "/home/longfor",
});
}
export function getHomePlusData() {
return cjRequest.get({
url: "/home/plus",
});
}
编写redux相关代码
jsx
import {
getHomeDiscountData,
getHomeGoodPriceData,
getHomeHighScoreData,
getHomeHotRecommendData,
getHomeLongforData,
getHomePlusData,
} from "@/services";
import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
export const fetchHomeDataAction = createAsyncThunk(
"fetchdata",
async (payload, { dispatch }) => {
getHomeGoodPriceData().then((res) => {
dispatch(changeGoodPriceInfoAction(res));
});
getHomeHighScoreData().then((res) => {
dispatch(changeHightScoreInfoAction(res));
});
getHomeDiscountData().then((res) => {
dispatch(changeDiscountInfoAction(res));
});
getHomeHotRecommendData().then((res) => {
dispatch(changeRecommendInfoAction(res));
});
getHomeLongforData().then((res) => {
dispatch(changeLongforInfoAction(res));
});
getHomePlusData().then((res) => {
dispatch(changePlusInfoAction(res));
});
}
);
const homeSlice = createSlice({
name: "home",
initialState: {
goodPriceInfo: {},
highScoreInfo: {},
discountInfo: {},
recommendInfo: {},
longforInfo: {},
plusInfo: {},
},
reducers: {
changeGoodPriceInfoAction(state, { payload }) {
state.goodPriceInfo = payload;
},
changeHightScoreInfoAction(state, { payload }) {
state.highScoreInfo = payload;
},
changeDiscountInfoAction(state, { payload }) {
state.discountInfo = payload;
},
changeRecommendInfoAction(state, { payload }) {
state.recommendInfo = payload;
},
changeLongforInfoAction(state, { payload }) {
state.longforInfo = payload;
},
changePlusInfoAction(state, { payload }) {
state.plusInfo = payload;
},
},
});
export const {
changeGoodPriceInfoAction,
changeHightScoreInfoAction,
changeDiscountInfoAction,
changeRecommendInfoAction,
changeLongforInfoAction,
changePlusInfoAction,
} = homeSlice.actions;
export default homeSlice.reducer;
希望用这种风格来展示数据
封装一个新组件,在views/home/c-cpns中创建一个home-section-v3用来存放该组件
jsx
import styled from "styled-components";
export const SectionV3Wrapper = styled.div``;
jsx
import React, { memo } from "react";
import PropTypes from "prop-types";
import ScrollView from "@/base-ui/scroll-view";
import SectionHeader from "@/components/section-header";
import SectionFooter from "@/components/section-footer";
import RoomItem from "@/components/room-item";
import { SectionV3Wrapper } from "./style";
const HomeSectionV3 = memo((props) => {
const { infoData } = props;
return (
<SectionV3Wrapper>
<SectionHeader title={infoData.title} subtitle={infoData.subtitle} />
<div className="room-list">
<ScrollView>
{infoData.list.map((item) => {
return <RoomItem itemData={item} key={item.id} itemWidth="20%" />;
})}
</ScrollView>
</div>
<SectionFooter name="plus" />
</SectionV3Wrapper>
);
});
HomeSectionV3.propTypes = {
infoData: PropTypes.object,
};
export default HomeSectionV3;
设置RoomItem组件不被压缩
jsx
import styled from "styled-components";
export const ItemWrapper = styled.div`
box-sizing: border-box;
/* width: 25%; */
width: ${(props) => props.itemWidth};
padding: 8px;
flex-shrink: 0;
.inner {
width: 100%;
}
//............
`;
Home组件统一设置margin-top,不用每个区域都设置
jsx
import styled from "styled-components";
export const HomeWrapper = styled.div`
> .content {
width: 1032px;
margin: 0 auto;
> div {
margin-top: 30px;
}
}
`;
首页使用HomeSectionV3
jsx
import { fetchHomeDataAction } from "@/store/modules/home";
import React, { memo, useEffect } from "react";
import { shallowEqual, useDispatch, useSelector } from "react-redux";
import HomeBanner from "./c-cpns/home-banner";
import HomeSectionV1 from "./c-cpns/home-section-v1";
import HomeSectionV2 from "./c-cpns/home-section-v2";
import HomeLongfor from "./c-cpns/home-longfor";
import HomeSectionV3 from "./c-cpns/home-section-v3";
import { isEmptyO } from "@/utils";
import { HomeWrapper } from "./style";
const Home = memo(() => {
// 从redux中获取数据
const {
goodPriceInfo,
highScoreInfo,
discountInfo,
recommendInfo,
longforInfo,
plusInfo,
} = useSelector(
(state) => ({
goodPriceInfo: state.home.goodPriceInfo,
highScoreInfo: state.home.highScoreInfo,
discountInfo: state.home.discountInfo,
recommendInfo: state.home.recommendInfo,
longforInfo: state.home.longforInfo,
plusInfo: state.home.plusInfo,
}),
shallowEqual
);
// 派发异步的事件:发送网络请求
const dispatch = useDispatch();
useEffect(() => {
dispatch(fetchHomeDataAction());
}, [dispatch]);
return (
<HomeWrapper>
<HomeBanner />
<div className="content">
{isEmptyO(discountInfo) && <HomeSectionV2 infoData={discountInfo} />}
{isEmptyO(recommendInfo) && <HomeSectionV2 infoData={recommendInfo} />}
{isEmptyO(longforInfo) && <HomeLongfor infoData={longforInfo} />}
{isEmptyO(goodPriceInfo) && <HomeSectionV1 infoData={goodPriceInfo} />}
{isEmptyO(highScoreInfo) && <HomeSectionV1 infoData={highScoreInfo} />}
{isEmptyO(plusInfo) && <HomeSectionV3 infoData={plusInfo} />}
</div>
</HomeWrapper>
);
});
export default Home;
项目-全部-全部页面的跳转和整体思路
SectionFooter添加点击事件跳转到全部页面查看更多房源
jsx
import PropTypes from "prop-types";
import React, { memo } from "react";
import { FooterWrapper } from "./style";
import IconMoreArrow from "@/assets/svg/icon_more_arrow";
import { useNavigate } from "react-router-dom";
const index = memo((props) => {
const { name = "" } = props;
let showMessage = "显示全部";
if (name) {
showMessage = `查看更多${name}房源`;
}
// 事件处理
const navigate = useNavigate();
function moreClickHandle() {
navigate("/entire");
}
return (
<FooterWrapper color={name ? "#00848A" : "#000"}>
<div className="info" onClick={moreClickHandle}>
<span>{showMessage}</span>
<IconMoreArrow />
</div>
</FooterWrapper>
);
});
index.propTypes = {
name: PropTypes.string,
};
export default index;
搭建全部页面结构
jsx
import styled from "styled-components";
export const EntireWrapper = styled.div``;
jsx
import React, { memo } from "react";
import { EntireWrapper } from "./style";
const Entire = memo(() => {
return (
<EntireWrapper>
<div className="filter">filter-section</div>
<div className="rooms">room-section</div>
<div className="pagination">pagination-section</div>
</EntireWrapper>
);
});
export default Entire;
项目-全部-全部页面的过滤条件展示和选中
在views/entire中创建c-cpns文件夹用于存放子组件
- 新建三个组件entire-filter/entire-rooms/entire-pagination,代码略
模拟filter数据
jsx
[
"人数",
"可免费取消",
"房源类型",
"价格",
"位置区域",
"闪定",
"卧室/床数",
"促销/优惠",
"更多筛选条件"
]
编写EntireFilter组件,实现点击高亮功能
jsx
import styled from "styled-components";
export const FilterWrapper = styled.div`
position: fixed;
z-index: 99;
left: 0;
right: 0;
top: 80px;
display: flex;
align-items: center;
height: 48px;
padding-left: 16px;
border-bottom: 1px solid #f2f2f2;
background-color: #fff;
.filter {
display: flex;
.item {
margin: 0 4px 0 8px;
padding: 6px 12px;
border: 1px solid #dce0e0;
border-radius: 4px;
color: #484848;
cursor: pointer;
&.active {
background: #008489;
border: 1px solid #008486;
color: #ffffff;
}
}
}
`;
jsx
import React, { memo, useState } from "react";
import { FilterWrapper } from "./style";
import filterData from "@/assets/data/filter_data.json";
import classNames from "classnames";
const EntireFilter = memo((props) => {
const [selectItems, setSelectItems] = useState([]);
function itemClickHandle(item) {
let newItems = [...selectItems];
if (newItems.includes(item)) { // 移除操作
const itemIndex = newItems.findIndex((filterItem) => filterItem === item);
newItems.splice(itemIndex, 1);
} else { // 添加操作
newItems.push(item);
}
setSelectItems(newItems);
}
return (
<FilterWrapper>
<div className="filter">
{filterData.map((item) => {
return (
<div
className={classNames("item", {
active: selectItems.includes(item),
})}
key={item}
onClick={(e) => itemClickHandle(item)}
>
{item}
</div>
);
})}
</div>
</FilterWrapper>
);
});
export default EntireFilter;
Entire组件导入三个子组件
jsx
import React, { memo } from "react";
import EntireFilter from "./c-cpns/entire-filter";
import EntirePagination from "./c-cpns/entire-pagination";
import EntireRooms from "./c-cpns/entire-rooms";
import { EntireWrapper } from "./style";
const Entire = memo(() => {
return (
<EntireWrapper>
<EntireFilter />
<EntireRooms />
<EntirePagination />
</EntireWrapper>
);
});
export default Entire;
项目-全部-房间列表数据获取和管理方式
编写redux相关代码(使用传统模式操作redux)
- 定义常量
jsx
export const CHANGE_CURRENT_PAGE = "entire/change_current_page";
export const CHANGE_ROOM_LIST = "entire/change_room_list";
export const CHANGE_TOTAL_COUNT = "entire/change_total_count";
- 编写reducer
jsx
import * as actionTypes from "./constants";
const initialState = {
currentPage: 3, // 当前页码
roomList: [], // 房间列表
totalCount: 0, // 总数据个数
};
function reducer(state = initialState, action) {
switch (action.type) {
case actionTypes.CHANGE_CURRENT_PAGE:
return { ...state, currentPage: action.currentPage };
case actionTypes.CHANGE_ROOM_LIST:
return { ...state, roomList: action.roomList };
case actionTypes.CHANGE_TOTAL_COUNT:
return { ...state, totalCount: action.totalCount };
default:
return state;
}
}
export default reducer;
- 编写action
jsx
import { getEntireRoomList } from "@/services/modules/entire";
import * as actionTypes from "./constants";
export const changeCurrentPageAction = (currentPage) => ({
type: actionTypes.CHANGE_CURRENT_PAGE,
currentPage,
});
export const changeRoomListAction = (roomList) => ({
type: actionTypes.CHANGE_ROOM_LIST,
roomList,
});
export const changeTotalCountAction = (totalCount) => ({
type: actionTypes.CHANGE_TOTAL_COUNT,
totalCount,
});
- 获取数据
- 在services/modules中新建模块entire.js
jsx
import cjRequest from "..";
export function getEntireRoomList(offset = 0, size = 20) {
return cjRequest.get({
url: "entire/list",
params: {
offset,
size,
},
});
}
- 发送请求在actionCreators.js中定义fetchRoomListAction方法
jsx
// ↑.............省略
export const fetchRoomListAction = () => {
// 新的函数
return async (dispatch, getState) => {
// 1.根据页码获取最新的数据
const currentPage = getState().entire.currentPage;
const res = await getEntireRoomList(currentPage*20);
// 2.获取到最新的数据,保存到redux的store中
const roomList = res.list;
const totalCount = res.totalCount;
dispatch(changeRoomListAction(roomList));
dispatch(changeTotalCountAction(totalCount));
};
};
- Entire组件中派发该action
jsx
import { fetchRoomListAction } from "@/store/modules/entire/actionCreators";
import React, { memo, useEffect } from "react";
import { useDispatch } from "react-redux";
import EntireFilter from "./c-cpns/entire-filter";
import EntirePagination from "./c-cpns/entire-pagination";
import EntireRooms from "./c-cpns/entire-rooms";
import { EntireWrapper } from "./style";
const Entire = memo(() => {
const dispatch = useDispatch();
useEffect(() => {
dispatch(fetchRoomListAction());
}, [dispatch]);
return (
<EntireWrapper>
<EntireFilter />
<EntireRooms />
<EntirePagination />
</EntireWrapper>
);
});
export default Entire;
项目-全部-房间列表数据的展示过程
点击logo跳转回首页
jsx
import React, { memo } from "react";
import { LeftWrapper } from "./style";
import IconLogo from "@/assets/svg/icon_logo";
import { useNavigate } from "react-router-dom";
const HeaderLeft = memo(() => {
const navigate = useNavigate();
function logoClickHandle() {
navigate("/home");
}
return (
<LeftWrapper>
<div className="logo" onClick={logoClickHandle}>
<IconLogo />
</div>
</LeftWrapper>
);
});
export default HeaderLeft;
编写EntireRooms组件展示数据
jsx
import styled from "styled-components";
export const RoomsWrapper = styled.div`
padding: 40px 20px;
.title {
font-size: 22px;
color: #222;
margin: 0 0 10px 10px;
}
.list {
display: flex;
flex-wrap: wrap;
}
`;
jsx
import RoomItem from "@/components/room-item";
import { useSelector } from "react-redux";
import React, { memo } from "react";
import { RoomsWrapper } from "./style";
const EntireRooms = memo((props) => {
// 从redux中获取roomList数据
const { roomList, totalCount } = useSelector((state) => ({
roomList: state.entire.roomList,
totalCount: state.entire.totalCount,
}));
return (
<RoomsWrapper>
<h2 className="title">{totalCount}家住宿</h2>
<div className="list">
{roomList.map((item) => {
return <RoomItem itemData={item} itemWidth="20%" key={item.id} />;
})}
</div>
</RoomsWrapper>
);
});
export default EntireRooms;
项目-全部-实现分页功能、请求数据的蒙版展示和细节调整
分页功能,使用material ui的分页Pagination组件
- 当分页改变时,传递页码重新请求数据
- 页码滚动回顶部
jsx
import styled from "styled-components";
export const PaginationWrapper = styled.div`
display: flex;
justify-content: center;
margin-top: 30px;
.page-info {
text-align: center;
.info {
margin-top: 20px;
}
}
.MuiPaginationItem-icon {
font-size: 20px;
}
.MuiPaginationItem-page {
margin: 0 9px;
&:hover {
text-decoration: underline;
}
}
.MuiPaginationItem-page.Mui-selected {
background-color: #222;
color: #fff;
&:hover {
background-color: #222;
}
}
`;
jsx
import React, { memo } from "react";
import Pagination from "@mui/material/Pagination";
import { PaginationWrapper } from "./style";
import { useDispatch, useSelector } from "react-redux";
import { fetchRoomListAction } from "@/store/modules/entire/actionCreators";
const EntirePagination = memo(() => {
const { currentPage, totalCount } = useSelector((state) => ({
currentPage: state.entire.currentPage,
totalCount: state.entire.totalCount,
}));
const count = Math.ceil(totalCount / 20);
const start = currentPage * 20 + 1;
const end = (currentPage + 1) * 20;
const dispatch = useDispatch();
function pageChangeHandle(event, newPage) {
window.scrollTo(0, 0);
dispatch(fetchRoomListAction(newPage - 1));
}
return (
<PaginationWrapper>
<div className="page-info">
<Pagination count={count} onChange={pageChangeHandle} />
<div className="info">
第 {start} - {end} 个房源, 共超过 {totalCount} 个
</div>
</div>
</PaginationWrapper>
);
});
export default EntirePagination;
修改action和reducer代码
- 添加一个loading用于实现请求数据过程中展示蒙版功能
jsx
export const CHANGE_CURRENT_PAGE = "entire/change_current_page";
export const CHANGE_ROOM_LIST = "entire/change_room_list";
export const CHANGE_TOTAL_COUNT = "entire/change_total_count";
export const CHANGE_LOADING = "entire/loading";
jsx
import { getEntireRoomList } from "@/services/modules/entire";
import * as actionTypes from "./constants";
export const changeCurrentPageAction = (currentPage) => ({
type: actionTypes.CHANGE_CURRENT_PAGE,
currentPage,
});
export const changeRoomListAction = (roomList) => ({
type: actionTypes.CHANGE_ROOM_LIST,
roomList,
});
export const changeTotalCountAction = (totalCount) => ({
type: actionTypes.CHANGE_TOTAL_COUNT,
totalCount,
});
export const changeLoadingAction = (isLoading) => ({
type: actionTypes.CHANGE_LOADING,
isLoading,
});
export const fetchRoomListAction = (page = 0) => {
// 新的函数
return async (dispatch, getState) => {
// 设置isLoading
dispatch(changeLoadingAction(true));
// 1.根据页码获取最新的数据
const res = await getEntireRoomList(page * 20);
dispatch(changeLoadingAction(false));
// 2.获取到最新的数据,保存到redux的store中
const roomList = res.list;
const totalCount = res.totalCount;
dispatch(changeCurrentPageAction(page));
dispatch(changeRoomListAction(roomList));
dispatch(changeTotalCountAction(totalCount));
};
};
jsx
import * as actionTypes from "./constants";
const initialState = {
currentPage: 3, // 当前页码
roomList: [], // 房间列表
totalCount: 0, // 总数据个数
};
function reducer(state = initialState, action) {
switch (action.type) {
case actionTypes.CHANGE_LOADING:
return { ...state, isLoading: action.isLoading };
case actionTypes.CHANGE_CURRENT_PAGE:
return { ...state, currentPage: action.currentPage };
case actionTypes.CHANGE_ROOM_LIST:
return { ...state, roomList: action.roomList };
case actionTypes.CHANGE_TOTAL_COUNT:
return { ...state, totalCount: action.totalCount };
default:
return state;
}
}
export default reducer;
EntireRooms组件编写添加蒙版元素
jsx
import styled from "styled-components";
export const RoomsWrapper = styled.div`
padding: 40px 20px;
.title {
font-size: 22px;
color: #222;
margin: 0 0 10px 10px;
}
.list {
display: flex;
flex-wrap: wrap;
}
> .cover {
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
background-color: rgba(255, 255, 255, 0.8);
}
`;
jsx
import RoomItem from "@/components/room-item";
import { shallowEqual, useSelector } from "react-redux";
import React, { memo } from "react";
import { RoomsWrapper } from "./style";
const EntireRooms = memo((props) => {
// 从redux中获取roomList数据
const { roomList, totalCount, isLoading } = useSelector(
(state) => ({
roomList: state.entire.roomList,
totalCount: state.entire.totalCount,
isLoading: state.entire.isLoading,
}),
shallowEqual
);
return (
<RoomsWrapper>
<h2 className="title">{totalCount}家住宿</h2>
<div className="list">
{roomList.map((item) => {
return <RoomItem itemData={item} itemWidth="20%" key={item._id} />;
})}
</div>
{isLoading && <div className="cover"></div>}
</RoomsWrapper>
);
});
export default EntireRooms;
项目-全部-RoomItem的轮播图实现效果
对RoomItem组件进行重构,使用antd的轮播图组件
- 如果是首页则展示一张图片,全部页则展示轮播图组件(判断有没有picture_urls)
- 实现左右切换
jsx
import PropTypes from "prop-types";
import React, { memo, useRef } from "react";
import { ItemWrapper } from "./style";
import { Carousel } from "antd";
import Rating from "@mui/material/Rating";
import IconArrowLeft from "@/assets/svg/icon_arrow_left";
import IconArrowRight from "@/assets/svg/icon_arrow_right";
const RoomItem = memo((props) => {
const { itemData, itemWidth = "25%" } = props;
const sliderRef = useRef();
// 事件处理的逻辑
function controlClickHandle(isRight = true) {
isRight ? sliderRef.current.next() : sliderRef.current.prev();
}
return (
<ItemWrapper
verifyColor={itemData?.verify_info?.text_color || "#39576a"}
itemWidth={itemWidth}
>
<div className="inner">
{!itemData.picture_urls ? (
<div className="cover">
<img src={itemData.picture_url} alt="" />
</div>
) : (
<div className="slider">
<div className="control">
<div
className="btn left"
onClick={(e) => controlClickHandle(false)}
>
<IconArrowLeft width="30" height="30" />
</div>
<div className="btn right" onClick={(e) => controlClickHandle()}>
<IconArrowRight width="30" height="30" />
</div>
</div>
<Carousel dots={false} ref={sliderRef}>
{itemData?.picture_urls?.map((item) => {
return (
<div className="cover" key={item}>
<img src={item} alt="" />
</div>
);
})}
</Carousel>
</div>
)}
<div className="desc">{itemData.verify_info.messages.join(" · ")}</div>
<div className="name">{itemData.name}</div>
<div className="price">¥{itemData.price}/晚</div>
<div className="bottom">
<Rating
value={itemData.star_rating ?? 5}
precision={0.1}
readOnly
sx={{ fontSize: "12px", color: "#008489" }}
/>
<span className="count">{itemData.reviews_count}</span>
{itemData?.bottom_info && (
<span className="extra">· {itemData.bottom_info.content}</span>
)}
</div>
</div>
</ItemWrapper>
);
});
RoomItem.propTypes = {
itemData: PropTypes.object,
itemWidth: PropTypes.string,
};
export default RoomItem;
jsx
import styled from "styled-components";
export const ItemWrapper = styled.div`
box-sizing: border-box;
/* width: 25%; */
width: ${(props) => props.itemWidth};
padding: 8px;
flex-shrink: 0;
.inner {
width: 100%;
}
.cover {
position: relative;
box-sizing: border-box;
padding: 66.66% 8px 0;
border-radius: 3px;
overflow: hidden;
img {
object-fit: cover;
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
}
}
.slider {
position: relative;
cursor: pointer;
&:hover {
.control {
display: flex;
}
}
.control {
position: absolute;
z-index: 1;
left: 0;
right: 0;
top: 0;
display: none;
justify-content: space-between;
bottom: 0;
color: #fff;
/* background-color: skyblue; */
.btn {
display: flex;
justify-content: center;
align-items: center;
width: 83px;
height: 100%;
background: linear-gradient(
to left,
transparent 0%,
rgba(0, 0, 0, 0.25) 100%
);
&.right {
background: linear-gradient(
to right,
transparent 0%,
rgba(0, 0, 0, 0.25) 100%
);
}
}
}
}
.desc {
margin: 10px 0 5px;
font-size: 12px;
font-weight: 700;
color: ${(props) => props.verifyColor};
}
.name {
font-size: 16px;
font-weight: 700;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.price {
margin: 8px 0;
}
.bottom {
display: flex;
align-items: center;
font-size: 12px;
font-weight: 600;
color: ${(props) => props.theme.text.primaryColor};
.count {
margin: 0 2px 0 4px;
}
.MuiRating-icon {
margin-right: -2px;
}
}
`;
项目-Indicator组件封装-结构的搭建
制作指示器组件,在base-ui文件夹中新建indicator文件夹存放指示器组件
jsx
import PropTypes from "prop-types";
import React, { memo } from "react";
import { IndicatorWrapper } from "./style";
const Indicator = memo((props) => {
return (
<IndicatorWrapper>
<div className="i-content">{props.children}</div>
</IndicatorWrapper>
);
});
Indicator.propTypes = {};
export default Indicator;
jsx
import styled from "styled-components";
export const IndicatorWrapper = styled.div`
.i-content {
display: flex;
overflow: hidden;
> * {
flex-shrink: 0;
}
}
`;
项目-Indicator组件封装-滚动位置的实现
jsx
import PropTypes from "prop-types";
import React, { memo, useEffect, useRef, useState } from "react";
import { IndicatorWrapper } from "./style";
const Indicator = memo((props) => {
const { selectIndex = 0 } = props;
const contentRef = useRef();
useEffect(() => {
// 1.获取selectIndex对应的item
const selectItemEl = contentRef.current.children[selectIndex];
const itemLeft = selectItemEl.offsetLeft;
const itemWidth = selectItemEl.clientWidth;
// 2.content的宽度
const contentWidth = contentRef.current.clientWidth;
// 获取selectIndex要滚动的距离
const distance = itemLeft + itemWidth * 0.5 - contentWidth * 0.5;
contentRef.current.style.transform = `translate(${-distance}px)`;
}, [selectIndex]);
return (
<IndicatorWrapper>
<div ref={contentRef} className="i-content">
{props.children}
</div>
</IndicatorWrapper>
);
});
Indicator.propTypes = {
selectIndex: PropTypes.number,
};
export default Indicator;
jsx
import styled from "styled-components";
export const IndicatorWrapper = styled.div`
overflow: hidden;
.i-content {
display: flex;
position: relative;
transition: transform 200ms ease;
> * {
flex-shrink: 0;
}
}
`;
新建一个demo页面组件进行测试
jsx
import Indicator from "@/base-ui/indicator";
import React, { memo, useState } from "react";
import { DemoWrapper } from "./style";
const Demo = memo(() => {
const names = ["abc", "cba", "nba", "mba", "aaa", "bbb", "ccc"];
const [selectIndex, setSelectIndex] = useState(0);
function toggleClickHandle(isNext = true) {
let newIndex = isNext ? selectIndex + 1 : selectIndex - 1;
if (newIndex < 0) newIndex = names.length - 1;
if (newIndex > names.length - 1) newIndex = 0;
setSelectIndex(newIndex);
}
return (
<DemoWrapper>
<div className="control">
<button onClick={(e) => toggleClickHandle(false)}>上一个</button>
<button onClick={(e) => toggleClickHandle(true)}>下一个</button>
</div>
<div className="list">
<Indicator selectIndex={selectIndex}>
{names.map((item) => {
return <button key={item}>{item}</button>;
})}
</Indicator>
</div>
</DemoWrapper>
);
});
export default Demo;
jsx
import styled from "styled-components";
export const DemoWrapper = styled.div`
.list {
width: 100px;
}
`;
项目-Indicator组件封装-左右特殊情况
要通过计算判断什么情况下需要居中,什么情况下不需要居中
- 左边的特殊情况处理:判断如果计算出来的距离为负数就不需要移动
- 右边的特殊情况处理:最多可以移动的距离
jsx
import PropTypes from "prop-types";
import React, { memo, useEffect, useRef, useState } from "react";
import { IndicatorWrapper } from "./style";
const Indicator = memo((props) => {
const { selectIndex = 0 } = props;
const contentRef = useRef();
useEffect(() => {
// 1.获取selectIndex对应的item
const selectItemEl = contentRef.current.children[selectIndex];
const itemLeft = selectItemEl.offsetLeft;
const itemWidth = selectItemEl.clientWidth;
// 2.content的宽度
const contentWidth = contentRef.current.clientWidth;
const contentScroll = contentRef.current.scrollWidth;
// 3.获取selectIndex要滚动的距离
let distance = itemLeft + itemWidth * 0.5 - contentWidth * 0.5;
// 4.特殊情况的处理
if (distance < 0) distance = 0; // 左边的特殊情况处理
const totalDistance = contentScroll - contentWidth;
if (distance > totalDistance) distance = totalDistance; // 右边的特殊情况处理
// 5.改变位置即可
contentRef.current.style.transform = `translate(${-distance}px)`;
}, [selectIndex]);
return (
<IndicatorWrapper>
<div ref={contentRef} className="i-content">
{props.children}
</div>
</IndicatorWrapper>
);
});
Indicator.propTypes = {
selectIndex: PropTypes.number,
};
export default Indicator;
项目-全部-RoomItem的指示器实现
在RoomItem中使用Indicator组件
jsx
import PropTypes from "prop-types";
import React, { memo, useRef, useState } from "react";
import { ItemWrapper } from "./style";
import { Carousel } from "antd";
import Rating from "@mui/material/Rating";
import IconArrowLeft from "@/assets/svg/icon_arrow_left";
import IconArrowRight from "@/assets/svg/icon_arrow_right";
import Indicator from "@/base-ui/indicator";
import classNames from "classnames";
const RoomItem = memo((props) => {
const { itemData, itemWidth = "25%" } = props;
const [selectIndex, setSelectIndex] = useState(0);
const sliderRef = useRef();
// 事件处理的逻辑
function controlClickHandle(isRight = true) {
// 上一个面板/下一个面板
isRight ? sliderRef.current.next() : sliderRef.current.prev();
// 最新的索引
let newIndex = isRight ? selectIndex + 1 : selectIndex - 1;
const length = itemData.picture_urls.length;
if (newIndex < 0) newIndex = length - 1;
if (newIndex > length - 1) newIndex = 0;
setSelectIndex(newIndex);
}
return (
<ItemWrapper
verifyColor={itemData?.verify_info?.text_color || "#39576a"}
itemWidth={itemWidth}
>
<div className="inner">
{!itemData.picture_urls ? (
<div className="cover">
<img src={itemData.picture_url} alt="" />
</div>
) : (
<div className="slider">
<div className="control">
<div
className="btn left"
onClick={(e) => controlClickHandle(false)}
>
<IconArrowLeft width="30" height="30" />
</div>
<div className="btn right" onClick={(e) => controlClickHandle()}>
<IconArrowRight width="30" height="30" />
</div>
</div>
<div className="indicator">
<Indicator selectIndex={selectIndex}>
{itemData?.picture_urls?.map((item, index) => {
return (
<div className="dot-item" key={item}>
<span
className={classNames("dot", {
active: selectIndex === index,
})}
></span>
</div>
);
})}
</Indicator>
</div>
<Carousel dots={false} ref={sliderRef}>
{itemData?.picture_urls?.map((item) => {
return (
<div className="cover" key={item}>
<img src={item} alt="" />
</div>
);
})}
</Carousel>
</div>
)}
<div className="desc">{itemData.verify_info.messages.join(" · ")}</div>
<div className="name">{itemData.name}</div>
<div className="price">¥{itemData.price}/晚</div>
<div className="bottom">
<Rating
value={itemData.star_rating ?? 5}
precision={0.1}
readOnly
sx={{ fontSize: "12px", color: "#008489" }}
/>
<span className="count">{itemData.reviews_count}</span>
{itemData?.bottom_info && (
<span className="extra">· {itemData.bottom_info.content}</span>
)}
</div>
</div>
</ItemWrapper>
);
});
RoomItem.propTypes = {
itemData: PropTypes.object,
itemWidth: PropTypes.string,
};
export default RoomItem;
jsx
import styled from "styled-components";
export const ItemWrapper = styled.div`
box-sizing: border-box;
/* width: 25%; */
width: ${(props) => props.itemWidth};
padding: 8px;
flex-shrink: 0;
.inner {
width: 100%;
}
.cover {
position: relative;
box-sizing: border-box;
padding: 66.66% 8px 0;
border-radius: 3px;
overflow: hidden;
img {
object-fit: cover;
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
}
}
.slider {
position: relative;
cursor: pointer;
&:hover {
.control {
display: flex;
}
}
.control {
position: absolute;
z-index: 1;
left: 0;
right: 0;
top: 0;
display: none;
justify-content: space-between;
bottom: 0;
color: #fff;
/* background-color: skyblue; */
.btn {
display: flex;
justify-content: center;
align-items: center;
width: 83px;
height: 100%;
background: linear-gradient(
to left,
transparent 0%,
rgba(0, 0, 0, 0.25) 100%
);
&.right {
background: linear-gradient(
to right,
transparent 0%,
rgba(0, 0, 0, 0.25) 100%
);
}
}
}
.indicator {
position: absolute;
z-index: 9;
bottom: 10px;
left: 0;
right: 0;
margin: 0 auto;
width: 30%;
.dot-item {
display: flex;
justify-content: center;
align-items: center;
width: 20%;
.dot {
width: 6px;
height: 6px;
background-color: #fff;
border-radius: 50%;
&.active {
width: 8px;
height: 8px;
}
}
}
}
}
.desc {
margin: 10px 0 5px;
font-size: 12px;
font-weight: 700;
color: ${(props) => props.verifyColor};
}
.name {
font-size: 16px;
font-weight: 700;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.price {
margin: 8px 0;
}
.bottom {
display: flex;
align-items: center;
font-size: 12px;
font-weight: 600;
color: ${(props) => props.theme.text.primaryColor};
.count {
margin: 0 2px 0 4px;
}
.MuiRating-icon {
margin-right: -2px;
}
}
`;
项目-Item点击跳转到详情页面
把item点击事件处理函数定义在props
- 把item数据传递出去
jsx
import PropTypes from "prop-types";
import React, { memo, useRef, useState } from "react";
import { ItemWrapper } from "./style";
import { Carousel } from "antd";
import Rating from "@mui/material/Rating";
import IconArrowLeft from "@/assets/svg/icon_arrow_left";
import IconArrowRight from "@/assets/svg/icon_arrow_right";
import Indicator from "@/base-ui/indicator";
import classNames from "classnames";
const RoomItem = memo((props) => {
const { itemData, itemWidth = "25%", itemClick } = props;
const [selectIndex, setSelectIndex] = useState(0);
const sliderRef = useRef();
// 事件处理的逻辑
function controlClickHandle(isRight = true) {
// 上一个面板/下一个面板
isRight ? sliderRef.current.next() : sliderRef.current.prev();
// 最新的索引
let newIndex = isRight ? selectIndex + 1 : selectIndex - 1;
const length = itemData.picture_urls.length;
if (newIndex < 0) newIndex = length - 1;
if (newIndex > length - 1) newIndex = 0;
setSelectIndex(newIndex);
}
function itemClickHandle() {
if (itemClick) itemClick(itemData);
}
return (
<ItemWrapper
verifyColor={itemData?.verify_info?.text_color || "#39576a"}
itemWidth={itemWidth}
onClick={itemClickHandle}
>
<div className="inner">
{!itemData.picture_urls ? (
<div className="cover">
<img src={itemData.picture_url} alt="" />
</div>
) : (
<div className="slider">
<div className="control">
<div
className="btn left"
onClick={(e) => controlClickHandle(false)}
>
<IconArrowLeft width="30" height="30" />
</div>
<div className="btn right" onClick={(e) => controlClickHandle()}>
<IconArrowRight width="30" height="30" />
</div>
</div>
<div className="indicator">
<Indicator selectIndex={selectIndex}>
{itemData?.picture_urls?.map((item, index) => {
return (
<div className="dot-item" key={item}>
<span
className={classNames("dot", {
active: selectIndex === index,
})}
></span>
</div>
);
})}
</Indicator>
</div>
<Carousel dots={false} ref={sliderRef}>
{itemData?.picture_urls?.map((item) => {
return (
<div className="cover" key={item}>
<img src={item} alt="" />
</div>
);
})}
</Carousel>
</div>
)}
<div className="desc">{itemData.verify_info.messages.join(" · ")}</div>
<div className="name">{itemData.name}</div>
<div className="price">¥{itemData.price}/晚</div>
<div className="bottom">
<Rating
value={itemData.star_rating ?? 5}
precision={0.1}
readOnly
sx={{ fontSize: "12px", color: "#008489" }}
/>
<span className="count">{itemData.reviews_count}</span>
{itemData?.bottom_info && (
<span className="extra">· {itemData.bottom_info.content}</span>
)}
</div>
</div>
</ItemWrapper>
);
});
RoomItem.propTypes = {
itemData: PropTypes.object,
itemWidth: PropTypes.string,
};
export default RoomItem;
在store/modules中新建一个detail.js模块,编写代码并合并reducer
jsx
import { createSlice } from "@reduxjs/toolkit";
const detailSlice = createSlice({
name: "detail",
initialState: {
detailInfo: {},
},
reducers: {
changeDetailInfoAction(state, { payload }) {
state.detailInfo = payload;
},
},
});
export const { changeDetailInfoAction } = detailSlice.actions;
export default detailSlice.reducer;
jsx
import { configureStore } from "@reduxjs/toolkit";
import homeReducer from "./modules/home";
import entireReducer from "./modules/entire";
import detailReducer from "./modules/detail";
const store = configureStore({
reducer: {
home: homeReducer,
entire: entireReducer,
detail: detailReducer,
},
});
export default store;
点击时派发action把item数据保存到redux,并跳转到详情页
jsx
import RoomItem from "@/components/room-item";
import { shallowEqual, useDispatch, useSelector } from "react-redux";
import React, { memo, useCallback } from "react";
import { RoomsWrapper } from "./style";
import { useNavigate } from "react-router-dom";
import { changeDetailInfoAction } from "@/store/modules/detail";
const EntireRooms = memo((props) => {
// 从redux中获取roomList数据
const { roomList, totalCount, isLoading } = useSelector(
(state) => ({
roomList: state.entire.roomList,
totalCount: state.entire.totalCount,
isLoading: state.entire.isLoading,
}),
shallowEqual
);
// 事件处理
const navigate = useNavigate();
const dispatch = useDispatch();
const itemClickHandle = useCallback(
(item) => {
dispatch(changeDetailInfoAction(item));
navigate("/detail");
},
[navigate, dispatch]
);
return (
<RoomsWrapper>
<h2 className="title">{totalCount}家住宿</h2>
<div className="list">
{roomList.map((item) => {
return (
<RoomItem
itemClick={itemClickHandle}
itemData={item}
itemWidth="20%"
key={item._id}
/>
);
})}
</div>
{isLoading && <div className="cover"></div>}
</RoomsWrapper>
);
});
export default EntireRooms;
在detail页面中获取数据
jsx
import React, { memo } from "react";
import { useSelector } from "react-redux";
const Detail = memo(() => {
const { detailInfo } = useSelector((state) => ({
detailInfo: state.detail.detailInfo,
}));
return <div>{detailInfo.name}</div>;
});
export default Detail;