文章目录
- 前言
- 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;
}
}
`;
`;
(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 issues
if (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 changes
handleChange();
// Listen matchMedia
if (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. 使用方式
// 在layout布局组件中引入
import Header from "@/components/Header";
// 使用
<Header />
4. Gif图效果展示
总结
下一篇讲【开始首页编码教学】。关注本栏目,将实时更新。