需求描述

实现分析
- 先采用absolute绝对定位
- 滚动超过锚点导航时,修改该导航定位为fixed
效果预览

实现方法
/** 锚点导航*/
import React from "react";
import styles from "./index.less";
import classnames from "classnames";
import { ActiveType } from "@17zwd/fe-egg-factory-page/detail/type";
interface PageState {
// 目录激活项
activeItem?: string;
// 是否吸顶
isSticky?: boolean;
}
class AnchorNav extends React.Component<any, PageState> {
private scrollTimer;
constructor(props) {
super(props);
this.state = {
activeItem: ActiveType.baseInfo,
isSticky: false,
};
}
componentDidMount(): void {
// 绑定页面滚动事件
window.addEventListener("scroll", this.onPageScroll);
}
componentWillUnmount(): void {
// 解绑页面滚动事件
window.removeEventListener("scroll", this.onPageScroll);
}
compare = (prop) => {
return function (a, b) {
const value1 = a[prop];
const value2 = b[prop];
return value1 - value2;
};
};
// 页面滚动事件
onPageScroll = (): void => {
if (this.scrollTimer) {
clearTimeout(this.scrollTimer);
}
this.scrollTimer = setTimeout(() => {
// 吸顶
const firstItemTop = document
.getElementById(ActiveType.baseInfo)
?.getBoundingClientRect().top;
if (firstItemTop && firstItemTop < 0) {
this.setState({
isSticky: true,
});
} else {
this.setState({
isSticky: false,
});
}
// 动态高亮
const viewTops: Array<any> = [];
for (const i in ActiveType) {
const id = ActiveType[i];
const ele = document.getElementById(id);
if (!ele) return;
const top = ele?.getBoundingClientRect().top;
if (top < 0) continue;
viewTops.push({
id,
top,
});
}
viewTops.sort(this.compare("top"));
this.setState({
activeItem: viewTops[0]?.id,
});
}, 90);
};
//跳转到的锚点
onScrollToAnchor = (activeItem: string): void => {
if (activeItem) {
// 找到锚点
const anchorElement = document.getElementById(activeItem);
// 如果对应id的锚点存在,就跳转到锚点
if (anchorElement) {
anchorElement.scrollIntoView({ block: "start", behavior: "smooth" });
this.setState({
activeItem,
});
}
}
};
getItemName = (en: string): string => {
let res = "";
switch (en) {
case ActiveType.baseInfo:
res = "基础信息";
break;
case ActiveType.certify:
res = "资质认证";
break;
case ActiveType.overview:
res = "综合概览";
break;
case ActiveType.remark:
res = "工厂简介";
break;
case ActiveType.pics:
res = "工厂实拍";
break;
case ActiveType.product:
res = "产品案例";
break;
default:
break;
}
return res;
};
renderItem = (): JSX.Element => {
const { activeItem } = this.state;
const items: Array<any> = [];
for (const i in ActiveType) {
const element = ActiveType[i];
if (element) {
items.push(element);
}
}
return (
<React.Fragment>
{items.map((item, index) => {
return (
<li
key={index}
onClick={() => this.onScrollToAnchor(item)}
className={activeItem === item ? styles.active : ""}
>
<span
className={classnames(styles.anchorItem, styles.anchorNoBorder)}
>
{this.getItemName(item)}
</span>
</li>
);
})}
</React.Fragment>
);
};
render(): JSX.Element {
const { isSticky } = this.state;
return (
<React.Fragment>
<div
className={classnames(
styles.mainAnchor,
isSticky ? styles.positionSticky : styles.positionAbsolute
)}
>
<ul className={styles.anchorList}>{this.renderItem()}</ul>
</div>
</React.Fragment>
);
}
}
export default AnchorNav;
@import "@/less/mixin.less";
// 导航
.positionSticky {
position: fixed;
margin-left: -124px;
z-index: 1;
}
.positionAbsolute {
position: absolute;
left: -124px;
}
.mainAnchor {
top: 0;
width: 124px;
.anchorList {
margin-bottom: 0;
padding: 0 24px;
width: 112px;
box-sizing: border-box;
background-color: @background-secondary-color;
> li {
position: relative;
color: @text-thrid-color;
transition: all 0.2s linear;
&:hover {
color: @main-primary-color;
}
}
.anchorItem {
display: block;
padding: 16px 0;
height: 53px;
font-size: 16px;
line-height: 21px;
box-sizing: border-box;
border-top: solid 1px @border-primary-color;
cursor: pointer;
}
.anchorNoBorder {
border-top: none;
}
.active {
color: @main-primary-color;
&::before {
position: absolute;
content: "";
left: -14px;
top: 24px;
width: 6px;
height: 6px;
border-radius: 50%;
background-color: @main-primary-color;
}
}
}
.navBox {
visibility: hidden;
}
}