楼主项目里没用ts ,赶时间写代码, 命名等不足请忽略, 不足之处请见谅!跪谢各位大佬给我点赞呀!!笔芯❤️
到顶部
/**
* constructor:
* distance 要滚动的距离
* step 每一步要走的距离
* delay 每一步要走的时间
*
* methods:
* scrollTo : (distance :每次要滚动的距离) : void => 滚动到顶部
* getTimer : 返回当前定时器 判断是否正在滚动中
* getInstance : 返回当前实例
*
* @class Scroll 滚动到顶部
*/
export class Scroll {
constructor(distance, step, delay) {
this.originDistance = distance
this.distance = distance
this.step = step
this.delay = delay
this.timer = null
}
static getInstance(distance, step = 40, delay = 15) {
if (!this.instance) {
this.instance = new Scroll(distance, step, delay)
}
return this.instance
}
scrollTo = (distance) => {
if (typeof distance === 'undefined') {
distance = 0
}
const nextDistance =
distance + this.step <= this.originDistance
? distance + this.step
: this.originDistance
this.timer = setTimeout(() => {
document.documentElement.scrollTop = nextDistance
document.body.scrollTop = nextDistance
if (nextDistance === this.originDistance) {
clearTimeout(this.timer)
this.timer = null
return
} else {
return this.scrollTo(nextDistance)
}
}, this.delay)
}
get getTimer() {
return this.timer
}
}
// 调用
const topHeight = this.topRef.getBoundingClientRect().height
const scrollToTop = Scroll.getInstance(topHeight)
if (scrollToTop.getTimer) {
return false
}
scrollToTop.scrollTo()
滚动到顶部 使用Window.requestAnimationFrame 更加顺滑 没有卡顿
export class Scroll {
constructor(distance, step) {
this.originDistance = distance
this.distance = distance
this.step = step
this.timer = null
}
static getInstance(distance, step = 50) {
if (!this.instance) {
this.instance = new Scroll(distance, step)
}
return this.instance
}
scrollTo = () => {
requestAFrame(()=>this._AnimationCb())
}
_AnimationCb = (distance) => {
if (typeof distance === "undefined") {
distance = 0
}
const nextDistance =
distance + this.step <= this.originDistance
? distance + this.step
: this.originDistance
document.documentElement.scrollTop = nextDistance
document.body.scrollTop = nextDistance
if (nextDistance !== this.originDistance) {
requestAFrame(() => this._AnimationCb(nextDistance))
}
}
get getTimer() {
return this.timer
}
}
滚动吸顶
import React, { useRef, useEffect, useState, useCallback } from "react"
// import PropTypes from "prop-types"
import classNames from "classnames"
import { debounce, getScrollTop, getOffsetTop } from "./utils"
import "./index.less"
export const StickyTab = props => {
const tabRef = useRef(null)
const [ shouldSticky, setShouldSticky ] = useState(false)
const [ inintialOffsetTop, setInintialOffsetTop ] = useState(0)
const [ emptyDivHeight, setEmptyDivHeight ] = useState(0)
useEffect(() => {
if (tabRef.current) {
const height = tabRef.current.getBoundingClientRect().height
setEmptyDivHeight(height)
setInintialOffsetTop(getOffsetTop(tabRef.current))
handleScroll()
}
return () => {}
}, [])
useEffect(
() => {
const handle = debounce(handleScroll, 100)
window.addEventListener("scroll", handle)
return () => {
window.removeEventListener("scroll", handle)
}
},
[ shouldSticky ]
)
const handleScroll = useCallback(
function() {
const scrollTop = getScrollTop()
const DIST = 10
if (!shouldSticky && scrollTop >= getOffsetTop(tabRef.current) - DIST) {
setShouldSticky(true)
} else if (shouldSticky && scrollTop < inintialOffsetTop - DIST) {
setShouldSticky(false)
}
},
[ shouldSticky ]
)
const cls = classNames("sticky-tab", {
"sticky-tab-fixed": shouldSticky,
})
return (
<div>
<div ref={tabRef} className={cls}>
{props.children}
</div>
<div style={{ height: emptyDivHeight + "px", display: shouldSticky ? "block" : "none" }} />
</div>
)
}
StickyTab.defaultProps = {
}
StickyTab.PropTypes = {}
export default StickyTab
// ./utils
// 获取屏幕的宽度
export const SW =
window.innerWidth ||
document.documentElement.clientWidth ||
document.body.clientWidth
// 获取屏幕的高度
export const SH =
window.innerHeight ||
document.documentElement.clientHeight ||
document.body.clientHeight
// 获取容器滚动的距离
export const getScrollTop = node => {
let scrollTop
if (typeof node === "undefined") {
scrollTop =
window.pageYOffset ||
document.documentElement.scrollTop ||
document.body.scrollTop
} else {
scrollTop = node.scrollTop
}
return scrollTop
}
// 获取元素距离可视区域顶部的 offsetTop
export const getOffsetTop = target => {
let top = target.offsetTop
let parent = target.offsetParent
while (parent) {
top += parent.offsetTop
parent = parent.offsetParent
}
return top
}
// 防抖
export function debounce(func, time) {
let timer = null
return function() {
const that = this
const argus = arguments
clearTimeout(timer)
timer = setTimeout(function() {
func.apply(that, argus)
}, time)
}
}
// ./index.less
.sticky-tab {
}
.sticky-tab-fixed{
position: fixed;
top: 0;
left: 0;
}
点击nav居中
import React, {
useEffect,
useRef,
useState,
useMemo,
useCallback,
memo,
} from "react"
import classNames from "classnames"
import PropTypes from "prop-types"
import { findShouldMoveToCenterMinIndex, clientWidth } from "./config"
import { debounce } from "../../utils"
import "./index.less"
export const NavBar = (props) => {
const [ width, setWidth ] = useState(null)
const [ activeIndex, setActiveIndex ] = useState(
props.originActiveIndex || 0
)
const [ shouldOverflowHidden, setShouldOverflowHidden ] = useState(false)
const [ translateX, setTranslateX ] = useState(0)
const contentRef = useRef(null)
useEffect(
() => {
if (!props.navs.length) return
const singleWidth = 72
let width = props.navs.length * singleWidth
if (width < clientWidth) {
width = clientWidth
setShouldOverflowHidden(true)
}
setWidth(width)
const onScroll = debounce(() => {
setTranslateX(0)
}, 100)
if (contentRef.current) {
contentRef.current.addEventListener("scroll", onScroll, false)
}
return () => {
contentRef.current.removeEventListener(
"scroll",
onScroll,
false
)
}
},
[ props.navs, translateX ]
)
const handleClick = useCallback(
(e, index, value) => {
if (index !== activeIndex) {
props.onClick(value)
}
setActiveIndex(index)
if (shouldOverflowHidden) return false
const ElWidth = e.target.getBoundingClientRect().width
const center = findShouldMoveToCenterMinIndex(
clientWidth,
ElWidth
)()
const min = center
const max = props.navs.length - center - 1
const contentScrollLeft = contentRef.current.scrollLeft
if (index <= min) {
// 开头区间的 点击元素的left < center 则需要移动到center
const leftDistance = 0 - contentScrollLeft
setTranslateX(-leftDistance)
} else if (index > min && index < max) {
// 只要是中间区间的 都要移动
const distance = ElWidth * index - clientWidth / 2
const leftDistance = distance - contentScrollLeft
setTranslateX(-leftDistance)
} else if (index >= max) {
const distance = ElWidth * props.navs.length - clientWidth
const leftDistance = distance - contentScrollLeft
setTranslateX(-leftDistance)
}
},
[ activeIndex ]
)
const navBarCls = useMemo(
() =>
classNames("newcomer_zone-navbar", {
"newcomer_zone-navbar-overflow": shouldOverflowHidden,
}),
[ shouldOverflowHidden ]
)
const contentCls = useMemo(
() =>
classNames("content", {
"content-overflow": shouldOverflowHidden,
}),
[ shouldOverflowHidden ]
)
return (
<div className={navBarCls} ref={contentRef}>
{width ? (
<div
style={{
width: width + "px",
transform: `translate(${translateX}px, 0px)`,
}}
className={contentCls}
>
{props.navs.map((nav, index) => {
const barCls = classNames({
active: activeIndex === index,
"bar-overflow": shouldOverflowHidden,
bar: !shouldOverflowHidden,
})
const style = {
width: width / props.navs.length + "px",
}
return (
<div
key={`${nav.value}-${index}`}
className={barCls}
style={style}
onClick={(e) =>
handleClick(e, index, nav.value)}
>
{nav.label}
</div>
)
})}
</div>
) : null}
</div>
)
}
NavBar.defaultProps = {
originActiveIndex: 0,
}
NavBar.propTypes = {
navs: PropTypes.array.isRequired,
originActiveIndex: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
]),
onClick: PropTypes.func.isRequired,
}
export default memo(NavBar)
// ./utils
function findShouldMoveToCenter(clientWidth, ElWidth) {
let index = 0
return function loop() {
if (index * ElWidth < clientWidth / 2) {
index++
return loop()
} else {
return index - 1
}
}
}
const clientWidth =
window.innerWidth ||
document.documentElement.clientWidth ||
document.body.clientWidth
export {findShouldMoveToCenter, clientWidth}
// ./index.less
.midou-navbar {
width: 100vw;
overflow-x: auto;
height: 40 * @rex;
background: rgba(255, 101, 118, 1);
.content {
display: flex;
.bar {
flex-grow: 0;
flex-shrink: 0;
text-align: center;
width: 54 * @rex;
height: 40 * @rex;
font-size: 13 * @rex;
font-weight: 400;
color: rgba(255, 255, 255, 1);
line-height: 40 * @rex;
}
.bar-overflow {
flex-grow: 0;
flex-shrink: 0;
text-align: center;
height: 40 * @rex;
font-size: 13 * @rex;
font-weight: 400;
color: rgba(255, 255, 255, 1);
line-height: 40 * @rex;
}
.active {
background: rgba(240, 57, 78, 1);
}
}
.content-overflow {
justify-content: space-between;
}
}
.midou-navbar-overflow {
overflow: hidden;
}
上啦加载
import React, { memo } from "react"
import loading from "./loading.png"
import { useScroll } from "./useScroll"
import "./index.less"
const text = {
LOADING: {
text: "正在拼命加载",
imgSrc: loading,
},
LOAMORE: { text: "上拉加载更多..." },
NOMORE: { text: "我也是有底线的" },
}
const LoadMore = props => {
useScroll(props)
let { loading } = props
loading = loading.toUpperCase()
const config = text[loading] || text["LOAMORE"]
return (
<div className="loading-midou">
<div className="loading-center">
{config && config.imgSrc ? (
<img src={config.imgSrc} alt="" className="loading-img" />
) : null}
<div className="loading-text">{config.text}</div>
</div>
</div>
)
}
export default memo(LoadMore)
// ./useScroll
import { useEffect, useRef } from "react"
import { debounce } from "../../utils"
// 获取容器滚动的距离
const getScrollTop = node => {
let scrollTop
if (typeof node === "undefined") {
scrollTop =
window.pageYOffset ||
document.documentElement.scrollTop ||
document.body.scrollTop
} else {
scrollTop = node.scrollTop
}
return scrollTop
}
// 获取屏幕的高度
const SH =
window.innerHeight ||
document.documentElement.clientHeight ||
document.body.clientHeight
const DIST = 100
export const useScroll = ({ onLoad, loading }) => {
const disable = useRef(false)
useEffect(() => {
function handleScroll(event) {
if (disable.current) return
const scrollTop = getScrollTop()
const scrollHeight =
document.documentElement.scrollHeight || document.body.scrollHeight
if (scrollTop >= scrollHeight - SH - DIST) {
disable.current = true
onLoad()
}
}
const onScroll = debounce(handleScroll, 200)
window.addEventListener("scroll", onScroll, false)
return () => {
window.removeEventListener("scroll", onScroll, false)
}
}, [])
useEffect(() => {
if (loading === "LOADMORE") {
disable.current = false
}
}, [loading])
}
// ./index.less
.loading-midou {
display: flex;
justify-content: center;
align-items: center;
color: #999;
.loading-center {
margin: 10* @rex 0;
}
.loading-text {
display: inline-block;
}
.loading-img {
display: inline-block;
width: 18 * @rex;
height: 18 * @rex;
animation: load 1.1s infinite linear;
margin-right: 4* @rex;
}
@keyframes load {
0% {
transform: rotate(0);
opacity: 1;
}
50% {
transform: rotate(180);
opacity: 0.5;
}
100% {
transform: rotate(360deg);
opacity: 1;
}
}
}
多导航时候 缓存scrollTop
// 判断点击是否需要吸顶(最小高度) 不设置为单例 所以记得unmount的时候销毁
// pravite
// 1. 缓存旧的 scrolltop ✅
// 2. 设置当前点击 active 的 scrolltop ✅
// 3. 判断是否吸顶
// 4. 设置列表的height min-height 为 100vh
// public
// 1. getScrollTop
// 2. onChange
// 3. setMinScrollTop
export class CacheListScrollTop {
constructor(...rest) {
this._init(...rest)
}
_init(minScrollTop, activeIndex, node) {
this.minScrollTop = minScrollTop
this.shouldSetMinScrollTop = false
this.activeIndex = activeIndex || 0 // 设置起始值
this.node = node
this.scrollTop = new Map()
this.DIST = 1
}
// 保存scrolltop
_cachePreviousScrollTop() {
const prevoisActiveIndex = this.activeIndex
const body = document.documentElement.scrollTop
? document.documentElement
: document.body
const node = this.node || body
const prevoisTop = Math.abs(node.getBoundingClientRect().top)
const scrollTop = new Map(this.scrollTop)
scrollTop.set(prevoisActiveIndex, prevoisTop)
this.scrollTop = scrollTop
}
_setNextScrollTop(index) {
this._cachePreviousScrollTop()
this.prevoisIndex = this.activeIndex
this.activeIndex = Number(index)
const activeNavScrollTop = this.scrollTop.get(Number(index)) || 0
// 设置最小值 scrollTop <= 最小高度 => 设置最小吸顶量
if (
activeNavScrollTop <= this.minScrollTop &&
this.shouldSetMinScrollTop
) {
this._setScrollTop(this.minScrollTop + this.DIST)
return false
}
// 设置最小值 scrollTop > 最小高度 => 设置 scrollTop
if (
activeNavScrollTop > this.minScrollTop &&
this.shouldSetMinScrollTop
) {
this._setScrollTop(activeNavScrollTop)
return false
}
// 不设置最小值 统一设置 scrollTop
const body = document.documentElement.scrollTop
? document.documentElement
: document.body
const node = this.node || body
const prevoisTop = Math.abs(node.getBoundingClientRect().top)
const keys = this.scrollTop.keys()
const scrollTop = new Map()
;([ ...keys ] || []).forEach((key) => {
scrollTop.set(key, prevoisTop)
})
this.scrollTop = scrollTop
this._setScrollTop(prevoisTop)
return false
}
_setScrollTop(scrollTop) {
if (this.node) {
this.node.scrollTop = scrollTop
return
}
document.documentElement.scrollTop = scrollTop
document.body.scrollTop = scrollTop
}
// 获取scrollTop 集合
get getScrollTop() {
return this.scrollTop
}
// scrollTop是否设置 最小值
setMinScrollTop(shouldSetMinScrollTop ) {
this.shouldSetMinScrollTop = shouldSetMinScrollTop
}
onChange(index) {
this._setNextScrollTop(index)
}
}
调用
// 如何调用
componentDidUpdate() {
super.componentDidUpdate()
if(!this.CacheListScrollTop && this.topRef){
const topHeight = this.topRef.getBoundingClientRect().height
this.CacheListScrollTop = new CacheListScrollTop(topHeight)
}
}
componentWillUnmount() {
super.componentWillUnmount()
this.CacheListScrollTop = null;
}