头部中间部分
import React, { memo } from "react";
import styleStrToObject from "./utils";
const IconSearch = memo(() => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 32 32"
aria-hidden="true"
role="presentation"
focusable="false"
style={styleStrToObject(
"display: block; fill: none; height: 12px; width: 12px; stroke: currentcolor; stroke-width: 5.33333; overflow: visible;"
)}
>
<path fill="none" d="M13 24a11 11 0 1 0 0-22 11 11 0 0 0 0 22zm8-3 9 9"></path>
</svg>
);
});
export default IconSearch;
import React, { memo } from "react";
import { CenterWrapper } from "./style";
import IconSearch from "@/assets/svg/icon-search";
const HeaderCenter = memo(() => {
return (
<CenterWrapper>
<div className="search-bar">
<div className="text">搜索房源和体验</div>
<span className="icon">
<IconSearch />
</span>
</div>
</CenterWrapper>
);
});
export default HeaderCenter;
import styled from "styled-components";
export const CenterWrapper = styled.div`
.search-bar {
display: flex;
align-items: center;
justify-content: space-between;
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.primary};
}
}
`;
总体样式调整: 新建一个common.less 文件,并在index.less文件中引入。
效果:
Profile点击面板切换的效果
import React, { memo, useEffect, useState } from "react";
import { RightWrapper } from "./style";
import IconGlobal from "@/assets/svg/icon-global";
import IconMenu from "@/assets/svg/icon-menu";
import IconAvatar from "@/assets/img/avatar.png";
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>登录</span>
<span>注册</span>
<span>
<IconGlobal />
</span>
</div>
<div className="profile" onClick={profileClickHandle}>
<span>
<IconMenu />
</span>
<span>
<img src={IconAvatar} alt="头像" />
</span>
{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;
import styled from "styled-components";
export const RightWrapper = styled.div`
flex: 1;
display: flex;
justify-content: flex-end;
padding-right: 40px;
align-items: center;
font-size: 14px;
font-weight: 600;
color: ${(props) => props.theme.text.primary};
.btns {
display: flex;
align-items: center;
span {
cursor: pointer;
padding: 12px 15px;
border-radius: 22px;
height: 18px;
line-height: 18px;
&:hover {
background-color: #f5f5f5;
}
}
}
.profile {
display: flex;
position: relative;
align-items: center;
margin-left: 5px;
border-radius: 22px;
border: 1px solid #ccc;
transition: box-shadow 0.2s ease;
width: 80px;
height: 42px;
box-sizing: border-box;
padding: 0px 10px 0 15px;
img {
width: 28px;
height: 28px;
border-radius: 50%;
margin-left: 10px;
}
/* transition: box-shadow 0.2s ease;
&:hover {
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.18);
} */
${(props) => props.theme.mixin.boxShadow}
.panel {
position: absolute;
top: 54px;
right: 0;
width: 240px;
background-color: #fff;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.18);
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 #ddd;
}
}
}
.profile > span {
cursor: pointer;
}
`;
效果:
Home页面banner图片展示
结构:
import React, { memo } from "react";
import { HomeWrapper } from "./style";
import HomeBanner from "./c-cpns/home-banner";
const Home = memo(() => {
return (
<HomeWrapper>
<HomeBanner />
</HomeWrapper>
);
});
export default Home;
import React, { memo } from "react";
import { BannerWrapper } from "./style";
const HomeBanner = memo(() => {
return <BannerWrapper>HomeBanner</BannerWrapper>;
});
export default HomeBanner;
import styled from "styled-components";
import coverImg from "@/assets/img/home_banner.png";
export const BannerWrapper = styled.div`
height: 529px;
/* background: url(${coverImg}) center/cover; */
background: url(${require("@/assets/img/home_banner.png")}) center/cover;
`;
效果:
webpack中两种引入图片资源方式
React脚手架中,如果我们直接../../这样子引入图片路径是显示不出来的, 解决方式,如下:
import coverImg from "@/assets/img/home_banner.png";
background: url(${require("@/assets/img/home_banner.png")}) center/cover;
首页-高性价比数据Redux获取和管理
- services准备接口:
新建home模块网络请求 modules -> home.js
在services -> index.js 文件中统一导出:
import zmRequest from "./request";
export default zmRequest;
export * from "./modules/home";
- redux中准备数据
import { getHomeGoodPriceData } from "@/services";
import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
// 异步
export const fetchHomeDataAction = createAsyncThunk("fetchdata", async (payload) => {
// console.log(payload);//从组件传递过来的参数
// payload 是在组件中触发fetchHomeDataAction(payload)携带的参数
// 然后传递给网络请求 getHomeGoodPriceData(payload)
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 }) {
//payload是 fetchHomeDataAction()的返回值
state.goodPriceInfo = payload;
},
},
});
export const { changeGoodPriceInfoAction } = homeSlice.actions;
export default homeSlice.reducer;
- extraReducers最新写法(上面的写法控制台会报警告)
import { getHomeGoodPriceData } from "@/services";
import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
// 异步
export const fetchHomeDataAction = createAsyncThunk("fetchdata", async (payload) => {
// payload 是在组件中触发fetchHomeDataAction(payload)携带的参数
// 然后传递给网络请求 getHomeGoodPriceData(payload)
// console.log(payload);//传参数
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 }) {
// //payload是 fetchHomeDataAction()的返回值
// state.goodPriceInfo = payload;
// },
// },
extraReducers: (builder) => {
// 异步
builder.addCase(fetchHomeDataAction.fulfilled, (state, { payload }) => {
state.goodPriceInfo = payload;
});
},
});
export const { changeGoodPriceInfoAction } = homeSlice.actions;
export default homeSlice.reducer;
- 在Home组件中派发action,从redux获取数据,展示
import React, { memo, useEffect } from "react";
import { shallowEqual, useDispatch, useSelector } from "react-redux";
import { HomeWrapper } from "./style";
import HomeBanner from "./c-cpns/home-banner";
import { fetchHomeDataAction } from "@/store/modules/home";
const Home = memo(() => {
// 从redux获取数据
const { goodPriceInfo } = useSelector(
(state) => ({
goodPriceInfo: state.home.goodPriceInfo,
}),
shallowEqual
);
// 派发异步的事件:发送网络请求
const dispatch = useDispatch();
useEffect(() => {
dispatch(fetchHomeDataAction("传参数data"));
}, [dispatch]);
return (
<HomeWrapper>
<HomeBanner />
<div className="content">
<h2>{goodPriceInfo.title}</h2>
<ul>
{goodPriceInfo.list?.map((item) => {
return <li key={item.id}>{item.name}</li>;
})}
</ul>
</div>
</HomeWrapper>
);
});
export default Home;
效果:
section-header组件部分封装
import PropTypes from "prop-types";
import React, { memo } from "react";
import { HeaderWrapper } from "./style";
const SectionHeader = memo((props) => {
// const { title, subtitle = "default subtitle data" } = 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;
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首页组件中使用封装的SectionHeader
import React, { memo, useEffect } from "react";
import { shallowEqual, useDispatch, useSelector } from "react-redux";
import { HomeWrapper } from "./style";
import HomeBanner from "./c-cpns/home-banner";
import { fetchHomeDataAction } from "@/store/modules/home";
import SectionHeader from "@/components/section-header";
const Home = memo(() => {
// 从redux获取数据
const { goodPriceInfo } = useSelector(
(state) => ({
goodPriceInfo: state.home.goodPriceInfo,
}),
shallowEqual
);
// 派发异步的事件:发送网络请求
const dispatch = useDispatch();
useEffect(() => {
dispatch(fetchHomeDataAction("传参数data"));
}, [dispatch]);
return (
<HomeWrapper>
{/* 轮播图 、banner图片*/}
<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;
import styled from "styled-components";
export const HomeWrapper = styled.div`
.content {
width: 1032px;
margin: 0 auto;
.good-price {
margin-top: 30px;
}
}
`;
效果:
底部footer
懒得写了,整个图片吧
房间的item封装和整体布局
import PropTypes from "prop-types";
import React, { memo } from "react";
import { ItemWrapper } from "./style";
const RoomItem = memo((props) => {
const { itemData } = props;
return <ItemWrapper>{itemData.name}</ItemWrapper>;
});
RoomItem.propTypes = {
itemData: PropTypes.object,
};
export default RoomItem;
import styled from "styled-components";
export const ItemWrapper = styled.div`
width: 25%;
padding: 8px;
box-sizing: border-box;
`;
父组件中使用RoomItem:
<ul className="room-list">
{goodPriceInfo.list?.slice(0, 8)?.map((item) => {
return <RoomItem itemData={item} key={item.id} />;
})}
</ul>
import styled from "styled-components";
export const HomeWrapper = styled.div`
.room-list {
display: flex;
flex-wrap: wrap;
margin: 0 -8px;
}
`;
效果:
子元素设置左右padding,要对齐,可以给外面的盒子设置margin负值
padding: 8px;
margin: 0 -8px;
item的布局展示过程
服务器返回图片大小不一致,如何展示:
import PropTypes from "prop-types";
import React, { memo } from "react";
import { ItemWrapper } from "./style";
const RoomItem = memo((props) => {
const { itemData } = props;
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;
import styled from "styled-components";
export const ItemWrapper = styled.div`
width: 25%;
padding: 8px;
box-sizing: border-box;
.inner {
width: 100%;
}
.cover {
position: relative;
box-sizing: border-box;
padding: 66.66% 8px 0;
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: #39576a; */
color: ${(props) => props.verifycolor};
}
.name {
font-size: 16px;
font-weight: 700;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
}
.price {
margin: 8px 0;
}
.bottom {
display: flex;
align-items: center;
font-size: 12px;
font-weight: 600;
color: ${(props) => props.theme.color.primary};
.count {
margin: 0 2px 0 4px;
}
.MuiRating-decimal {
margin-right: -3px;
}
}
`;
效果: