React18 项目 完结篇 最新antd(^5.7.0)的使用 room-item中轮播图组件使用 指示器封装

263 阅读3分钟

Antd使用

  • npm install antd --save
  • 版本"antd": "^5.7.0",

image.png

  • 在index.js文件中配置中文,不用引入css文件
import { ConfigProvider } from "antd";
import zhCN from "antd/locale/zh_CN";

<ConfigProvider locale={zhCN}>
    <App />
</ConfigProvider>
  • 组件中使用,按需引入即可
import { Carousel } from "antd";
const contentStyle = {
  height: "160px",
  color: "#fff",
  lineHeight: "160px",
  textAlign: "center",
  background: "#364d79",
};

 {/* antd */}
 <Carousel autoplay>
        <div>
          <h3 style={contentStyle}>1</h3>
        </div>
        <div>
          <h3 style={contentStyle}>2</h3>
        </div>
        <div>
          <h3 style={contentStyle}>3</h3>
        </div>
        <div>
          <h3 style={contentStyle}>4</h3>
        </div>
 </Carousel>

效果:

image.png

room-item中轮播图组件使用

import PropTypes from "prop-types";
import React, { memo, useRef } from "react";
import { ItemWrapper } from "./style";

import Rating from "@mui/material/Rating";
import { Carousel } from "antd";
import IconArrowLeft from "@/assets/svg/icon_arrow_left";
import IconArrow from "@/assets/svg/icon_arrow";

const RoomItem = memo((props) => {
  const { itemData, itemWidth = "25%" } = props;
  const swiperRef = useRef(); //Carousel 组件实例

  // 事件处理逻辑
  function controlClickHandle(isRight = true) {
    isRight ? swiperRef.current.next() : swiperRef.current.prev();
  }
  return (
    <ItemWrapper itemWidth={itemWidth} verifycolor={itemData?.verify_info?.text_color || "#39576a"}>
      <div className="inner">
        {/* <div className="cover">
          <img src={itemData.picture_url} alt="" />
        </div> */}
        <div className="swiper">
          <div className="control">
            <div className="btn left" onClick={(e) => controlClickHandle(false)}>
              <IconArrowLeft width="30" height="30" />
            </div>
            <div className="btn right" onClick={(e) => controlClickHandle(true)}>
              <IconArrow width="30" height="30" />
            </div>
          </div>
          <Carousel dots={false} autoplay ref={swiperRef}>
            {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: "#00848A" }}
          />
          <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;
import styled from "styled-components";

export const ItemWrapper = styled.div`
  flex-shrink: 0;

  /* width: 25%; */
  /* width: 33.33%; */
  width: ${(props) => props.itemWidth};
  padding: 8px;
  box-sizing: border-box;
  .inner {
    width: 100%;
  }

  .swiper {
    position: relative;
    cursor: pointer;
    &:hover {
      .control {
        display: flex;
      }
    }
    .control {
      position: absolute;
      z-index: 1;
      left: 0;
      right: 0;
      top: 0;
      bottom: 0;
      display: none;
      justify-content: space-between;
      color: #fff;

      .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%);
        }
      }
    }
  }

  .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%;
      object-fit: cover;
    }
  }
  .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: flex-start;
    font-size: 12px;
    font-weight: 600;
    color: ${(props) => props.theme.text.primary};
    .count {
      margin: 0 2px 0 4px;
    }
    .MuiRating-decimal {
      margin-right: -3px;
    }
  }
`;

效果:点击箭头可切换图片

image.png

指示器封装

先看效果:点击箭头,指示器规律滚动

image.png

import PropTypes from "prop-types";
import React, { memo, useEffect, useRef } 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.获取selectItemEl要滚动的距离
    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 className="i-content" ref={contentRef}>
        {props.children}
      </div>
    </IndicatorWrapper>
  );
});

Indicator.propTypes = {
  selectIndex: PropTypes.number,
};
export default Indicator;
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;
    }
  }
`;

组件中使用:

import PropTypes from "prop-types";
import React, { memo, useRef, useState } from "react";
import { ItemWrapper } from "./style";

import Rating from "@mui/material/Rating";
import { Carousel } from "antd";
import IconArrowLeft from "@/assets/svg/icon_arrow_left";
import IconArrow from "@/assets/svg/icon_arrow";
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 swiperRef = useRef(); //Carousel 组件实例

  // 事件处理逻辑
  function controlClickHandle(isRight = true) {
    // 上一个面板/下一个面板
    isRight ? swiperRef.current.next() : swiperRef.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);
  }

  const pictureElement = (
    <div className="cover">
      <img src={itemData.picture_url} alt="" />
    </div>
  );

  const swiperElement = (
    <div className="swiper">
      <div className="control">
        <div className="btn left" onClick={(e) => controlClickHandle(false)}>
          <IconArrowLeft width="30" height="30" />
        </div>
        <div className="btn right" onClick={(e) => controlClickHandle(true)}>
          <IconArrow width="30" height="30" />
        </div>
      </div>
      {/* 指示器 */}
      <div className="indictor">
        <Indicator selectIndex={selectIndex}>
          {itemData?.picture_urls?.map((item, index) => {
            return (
              <div className="item" key={item}>
                <span className={classNames("dot", { active: selectIndex === index })}></span>
              </div>
            );
          })}
        </Indicator>
      </div>
      <Carousel dots={false} autoplay ref={swiperRef}>
        {itemData?.picture_urls?.map((item) => {
          return (
            <div className="cover" key={item}>
              <img src={item} alt="" />
            </div>
          );
        })}
      </Carousel>
    </div>
  );

  return (
    <ItemWrapper itemWidth={itemWidth} verifycolor={itemData?.verify_info?.text_color || "#39576a"}>
      <div className="inner">
        {!itemData.picture_urls ? pictureElement : swiperElement}
        <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: "#00848A" }}
          />
          <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;
    .indictor {
      position: absolute;
      z-index: 9;
      width: 30%;
      left: 0;
      right: 0;
      bottom: 10px;
      margin: 0 auto;
      .item {
        display: flex;
        justify-content: center;
        align-items: center;
        width: 14.29%;

        .dot {
          width: 6px;
          height: 6px;
          background-color: #fff;
          border-radius: 50%;

          &.active {
            width: 8px;
            height: 8px;
            background-color: purple;
          }
        }
      }
    }

item点击跳转到详情

如果在子组件中绑定点击事件,在home页,也会跳转,所以我们可以传递一个函数(使用useCallback()包裹,进行性能优化)到子组件中,让父组件控制点击子组件跳转不跳转,子组件可以传递参数到父组件。

子组件:

  function itemClickHandle(){
    if(itemClick) itemClick(itemData)
  }
  
      <ItemWrapper itemWidth={itemWidth} verifycolor={itemData?.verify_info?.text_color || "#39576a"}>
      <div className="inner" onClick={itemClickHandle}>
        {!itemData.picture_urls ? pictureElement : swiperElement}
        <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: "#00848A" }}
          />
          <span className="count">{itemData.reviews_count}</span>
          {itemData?.bottom_info && <span className="extra">·{itemData?.bottom_info?.content}</span>}
        </div>
      </div>
    </ItemWrapper>

子组件全部代码

import PropTypes from "prop-types";
import React, { memo, useRef, useState } from "react";
import { ItemWrapper } from "./style";

import Rating from "@mui/material/Rating";
import { Carousel } from "antd";
import IconArrowLeft from "@/assets/svg/icon_arrow_left";
import IconArrow from "@/assets/svg/icon_arrow";
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 swiperRef = useRef(); //Carousel 组件实例

  // 事件处理逻辑
  function controlClickHandle(isRight = true) {
    // 上一个面板/下一个面板
    isRight ? swiperRef.current.next() : swiperRef.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)
  }

  const pictureElement = (
    <div className="cover">
      <img src={itemData.picture_url} alt="" />
    </div>
  );

  const swiperElement = (
    <div className="swiper">
      <div className="control">
        <div className="btn left" onClick={(e) => controlClickHandle(false)}>
          <IconArrowLeft width="30" height="30" />
        </div>
        <div className="btn right" onClick={(e) => controlClickHandle(true)}>
          <IconArrow width="30" height="30" />
        </div>
      </div>
      {/* 指示器 */}
      <div className="indictor">
        <Indicator selectIndex={selectIndex}>
          {itemData?.picture_urls?.map((item, index) => {
            return (
              <div className="item" key={item}>
                <span className={classNames("dot", { active: selectIndex === index })}></span>
              </div>
            );
          })}
        </Indicator>
      </div>
      <Carousel dots={false} autoplay ref={swiperRef}>
        {itemData?.picture_urls?.map((item) => {
          return (
            <div className="cover" key={item}>
              <img src={item} alt="" />
            </div>
          );
        })}
      </Carousel>
    </div>
  );

  return (
    <ItemWrapper itemWidth={itemWidth} verifycolor={itemData?.verify_info?.text_color || "#39576a"}>
      <div className="inner" onClick={itemClickHandle}>
        {!itemData.picture_urls ? pictureElement : swiperElement}
        <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: "#00848A" }}
          />
          <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;

父组件:

import React, { memo, useCallback } from "react";
import { RoomsWrapper } from "./style";
import { shallowEqual, useDispatch, useSelector } from "react-redux";
import RoomItem from "@/components/room-item";
import { useNavigate } from "react-router-dom";
import { changeDetailInfoAction } from "@/store/modules/detail";

const EntireRooms = memo((props) => {
  // 从redux中获取数据
  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(
    (itemData) => {
      console.log("123=> ", itemData);
      dispatch(changeDetailInfoAction(itemData));
      navigate("/detail");
    },
    [navigate, dispatch]
  );
  return (
    <RoomsWrapper>
      <div className="title">{totalCount}多处住所</div>
      <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;

父组件中派发action(将itemData存储到redux中,并跳转到详情页)

changeDetailInfoAction:

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;

详情页获取数据展示:

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;

detail页效果:

image.png

至此,项目告一段落了。