文章目录
- 前言
- Header头部相关组件
- 1. 功能分析
- 2. 相关组件代码+详细注释
- 3. 使用方式
- 4. Gif图效果展示
- 总结
前言
在这篇博客中,我们将封装一个头部组件,根据不同设备类型来显示不同的导航菜单,会继续使用 React hooks 和styled-components库来构建这个组件,此外,也会实现切换国际化功能。
Header头部相关组件
1. 功能分析
(1)根据用户的设备类型(移动设备或PC设备),动态渲染不同的导航菜单。
(2)封装的 useIsMobile hook函数,判断用户的设备类型
(3)封装导航菜单 NavMenu组件,根据是否是移动设备来决定渲染哪个导航菜单
(4)封装国际化语言切换弹窗组件,实现切换语言功能
(5)移动端导航菜单按钮由三个div元素组成,点击后元素添加动画效果,并控制导航菜单显示与否
(5)使用到的全局组件请看之前文章国际化配置、全局常用组件弹窗Dialog封装、全局常用组件Select封装、全局常用组件Link封装
2. 相关组件代码+详细注释
(1)首先,先来封装一个导航菜单组件
// @/components/Header/NavMenu/index.tsx
import { memo, FC } from "react";
import classNames from "classnames";
import { useTranslation } from "react-i18next";
import Link from "@/components/Link";
import LanguagePanel from "@/components/Header/LanguagePanel";
import { MobileMenuList, PCMenuList } from "./styled";interface navListMap {name: string; // 菜单名称url: string; // 菜单链接地址
}/*** 获取导航菜单列表* @returns {navListMap[]} 导航菜单列表*/
const useNavList = () => {const { t } = useTranslation();const list: navListMap[] = [{name: t("navbar.home"),url: "/home",},{name: t("navbar.nervos_dao"),url: "/nervosdao",},{name: t("navbar.tokens"),url: "/tokens",},{name: t("navbar.fee_rate"),url: "/fee-rate-tracker",},{name: t("navbar.charts"),url: "/charts",},];return list;
};/*** 移动端导航菜单* @returns {JSX.Element}*/
const MobileMenu: FC<{ navList: navListMap[] }> = ({ navList }) => {return (<MobileMenuList>{navList.map((item) => (<Link className={classNames("mobile-menu-list")} to={item.url ?? "/"} key={item.name}>{item.name}</Link>))}<LanguagePanel /> {/* 语言选择组件 */}</MobileMenuList>);
};
/*** 桌面端导航菜单* @returns {JSX.Element}*/
const PCMenu: FC<{ navList: navListMap[] }> = ({ navList }) => {return (<><PCMenuList>{navList.map((item) => (<Link className={classNames("nav-item")} to={item.url ?? "/"} key={item.name}>{item.name}</Link>))}</PCMenuList><LanguagePanel /> {/* 语言选择组件 */}</>);
};/*** 导航菜单组件* @param {boolean} isMobile - 是否是移动端* @returns {JSX.Element} 导航菜单组件*/
export default memo<{ isMobile: boolean }>(({ isMobile }) => {const navList = useNavList();return isMobile ? <MobileMenu navList={navList} /> : <PCMenu navList={navList}></PCMenu>;
});
-----------------------------------------------------------------------------
// @/components/Header/NavMenu/styled.tsx
import styled from "styled-components";
export const MobileMenuList = styled.div`width: 100vw;height: calc(100vh - var(--cd-navbar-height));position: absolute;top: var(--cd-navbar-height);box-sizing: border-box;left: 0;background: #2b2c30;.mobile-menu-list {display: flex;flex-direction: column;align-items: flex-start;margin: 20px 40px;color: #fff;}.language-switch {margin-left: 40px;text-align: left;}
`;
export const PCMenuList = styled.div`display: flex;flex: 1;min-width: 0;.nav-item {display: flex;align-items: center;flex-shrink: 0;margin-right: 50px;color: white;&:hover {color: var(--cd-primary-color);}}
`;
(2)接下来我们开始封装国际化语言切换组件,在其中会引用到之前文章封装的Dialog组件和Select组件
// @/components/Header/LanguagePanel/index.tsx
import { useState, memo } from "react";
import { useLocation } from "react-router";
import { useTranslation } from "react-i18next";
import { SupportedLngs, useChangeLanguage } from "@/config/i18n";
import { LanguageContainer } from "./styled";
import Dialog from "@/pages/components/commonDialog";
import Select from "@/components/Select";
type Option = {label: string; // 选项的显示文本value: string; // 选项的值
};
export default memo(() => {// 获取当前语言const { pathname } = useLocation();const currentLanguage = pathname.split("/")[1];// 获取语言切换的钩子函数const { changeLanguage } = useChangeLanguage();// 获取国际化的钩子函数const { t } = useTranslation();// 控制语言弹框的显示隐藏const [languageModalVisible, setLanguageModalVisible] = useState(false);// 当前选中的语言const [language, setLanguage] = useState(currentLanguage);// 获取所有支持的语言const lngOptions: Option[] = SupportedLngs.map((lng) => ({value: lng,label: t(`navbar.language_${lng}`),}));// 关闭切换语言弹框const handlerClose = () => {setLanguageModalVisible(!languageModalVisible);};// 确定切换语言const handlerDone = () => {return new Promise((resolve) => {changeLanguage(language);handlerClose();resolve(true);});};// 切换语言const handlerLanguageChange = (value: string) => {setLanguage(value);};// 打开语言弹框const handlerOpenLanguage = () => {setLanguageModalVisible(!languageModalVisible);};// 语言选择弹框return (<>{/* 语言切换 */}<LanguageContainer className={classNames("language-switch")} onClick={handlerOpenLanguage}><i className="iconfont icon-guojihua"></i><span>{t("navbar.language")}</span></LanguageContainer>{/* 语言选择弹框 */}<Dialog title={t("navbar.language_switch")} doneText={t("button.confirm")} show={languageModalVisible} onClose={handlerClose} onDoneClick={handlerDone}><Select options={lngOptions} onChange={handlerLanguageChange} defaultValue={currentLanguage} placeholder={t("placeholder.default")}></Select></Dialog></>);
});
-----------------------------------------------------------------------------
// @/components/Header/LanguagePanel/styled.tsx
import styled from "styled-components";
export const LanguageContainer = styled.div`color: #ffffff;cursor: pointer;span {margin-left: 5px;}
`;
(3)最后一步,封装父组件Header组件,并引入NavMenu组件和LanguagePanel组件
// @/components/Header/index.tsx
import { FC, useState } from "react";
import classNames from "classnames";
import LogoIcon from "@/assets/headerLogo.png";
import { Header, Logo, MobileMenuContainer, HeaderContainer } from "./styled";
import { useIsMobile } from "@/hooks";
import NavMenu from "./NavMenu";// 头部组件
const HeaderComponent: FC = () => {// 判断是否是移动端const isMobile = useIsMobile();// PC端导航菜单组件const PCMenus: FC = () => {return <NavMenu isMobile={isMobile} />;};// 移动端导航菜单const MobileMenus: FC = () => {// 控制移动端菜单是否显示的状态const [mobileMenuVisible, setMobileMenuVisible] = useState<boolean>(false);return (<MobileMenuContainer><div className={mobileMenuVisible ? "close" : ""} onClick={() => setMobileMenuVisible(!mobileMenuVisible)}><div className={classNames("firstLine")} /><div className={classNames("secondLine")} /><div className={classNames("thirdLine")} /></div>{mobileMenuVisible && isMobile && <NavMenu isMobile={isMobile} />}</MobileMenuContainer>);};return (<HeaderContainer><Header><Logo to="/"><img src={LogoIcon} alt="logo" /></Logo>{isMobile ? <MobileMenus /> : <PCMenus />}</Header></HeaderContainer>);
};export default HeaderComponent;
------------------------------------------------------------------------------
// @/components/Header/styled.tsx
import styled from "styled-components";
import Link from "../Link";
export const HeaderContainer = styled.div`position: sticky;top: 0;z-index: 10;display: flex;flex-direction: column;
`;
export const Header = styled.div`width: 100%;min-height: var(--cd-navbar-height);background-color: #2b2c30;overflow: visible;display: flex;align-items: center;flex-wrap: wrap;padding: 0 120px;@media (max-width: 1440px) {padding: 0 100px;}@media (max-width: 1200px) {padding: 0 45px;}@media (max-width: 780px) {padding: 0 18px;}
`;export const Logo = styled(Link)`display: flex;align-items: center;margin-right: 40px;img {width: 140px;}
`;export const MobileMenuContainer = styled.div`display: flex;justify-content: flex-end;flex: 1;.firstLine,.secondLine,.thirdLine {width: 18px;height: 2px;background-color: #fff;margin: 5px 0;transition: 0.4s;}.close {.firstLine {transform: rotate(45deg) translate(6px, 3px);}.secondLine {opacity: 0;}.thirdLine {transform: rotate(-45deg) translate(6px, -4px);}}.mobile-menu {width: 100vw;height: calc(100vh - var(--cd-navbar-height));position: absolute;top: var(--cd-navbar-height);box-sizing: border-box;left: 0;background: #2b2c30;.mobile-menu-list {display: flex;flex-direction: column;align-items: flex-start;margin: 20px 40px;color: #fff;// overflow: auto;// overscroll-behavior: contain;}}
`;`;
(4)贴上封装的判断设备的钩子函数,自行取用即可
import { useEffect, useState } from "react";
import variables from "@/styles/variables.module.scss";/*** copied from https://usehooks-ts.com/react-hook/use-media-query*/
export function useMediaQuery(query: string): boolean {const getMatches = (query: string): boolean => {// Prevents SSR issuesif (typeof window !== "undefined") {return window.matchMedia(query).matches;}return false;};const [matches, setMatches] = useState<boolean>(getMatches(query));useEffect(() => {const matchMedia = window.matchMedia(query);const handleChange = () => setMatches(getMatches(query));// Triggered at the first client-side load and if query changeshandleChange();// Listen matchMediaif (matchMedia.addListener) {matchMedia.addListener(handleChange);} else {matchMedia.addEventListener("change", handleChange);}return () => {if (matchMedia.removeListener) {matchMedia.removeListener(handleChange);} else {matchMedia.removeEventListener("change", handleChange);}};}, [query]);return matches;
}/*** 移动端断点,单位为px*/
export const mobileBreakPoint = Number(variables.mobileBreakPoint.replace("px", ""));/*** 是否是大型屏幕*/
export const useIsXXLBreakPoint = () => useMediaQuery(`(max-width: ${variables.xxlBreakPoint})`);/*** 是否处是移动端*/
export const useIsMobile = () => useMediaQuery(`(max-width: ${variables.mobileBreakPoint})`);/*** 是否处于最大宽度为extraLargeBreakPoint的断点,如果exact为true,则需要同时不处于mobileBreakPoint的断点*/
export const useIsExtraLarge = (exact = false) => {const isMobile = useIsMobile();const isExtraLarge = useMediaQuery(`(max-width: ${variables.extraLargeBreakPoint})`);return !exact ? isExtraLarge : isExtraLarge && !isMobile;
};
3. 使用方式
// 引入组件
import Loading from "@/components/Loading";
// 使用
<Loading size="small" /> {/* 小尺寸loading */}
<Loading /> {/* 默认尺寸loading */}
<Loading size="large" /> {/* 大尺寸loading */}
4. Gif图效果展示
总结
下一篇讲【开始首页编码教学】。关注本栏目,将实时更新。