React项目,Profile点击面板效果,webpack中两种引入图片资源方式,Redux获取和管理,section-header组件部分封装

38 阅读2分钟

头部中间部分

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文件中引入。

image.png

效果:

image.png

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;
  }
`;

效果:

image.png

Home页面banner图片展示

结构:

image.png

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;
`;

效果:

image.png

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 image.png

在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;

效果:

image.png

section-header组件部分封装

image.png

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;
    }
  }
`;

效果:

image.png

底部footer

懒得写了,整个图片吧

房间的item封装和整体布局

image.png

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;
      }
`;

效果:

image.png

子元素设置左右padding,要对齐,可以给外面的盒子设置margin负值

padding: 8px;
margin: 0 -8px;

item的布局展示过程

服务器返回图片大小不一致,如何展示:

image.png

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;
    }
  }
`;

效果:

image.png