Files
Profeto/frontend/src/lib/useScroll.ts
T
shangfangjian 2cfc9ccc0f refactor(ui): 合并双组件库为 components/ui 基元层,设计令牌变量化,报头字体子集自托管
- 新建 components/ui/(Button/Input/Select/Modal/Tabs/Spinner/Skeleton 等),变体用联合类型约束,根治幻影变体
- admin/components.tsx 与 matches/ui.tsx 改为再导出壳,消除 Spinner 三份重复
- 日期工具收口到 lib/date.ts,滚动监听收口到 lib/useScroll.ts
- index.css 引入 :root RGB 三元组令牌,tailwind.config 接 <alpha-value>(修复透明度修饰符静默失效)
- 刘建毛草 5MB 全字库(jsDelivr 未锁版) → 1KB 自托管子集(仅「先知」二字,unicode-range 限定)
2026-09-22 23:44:46 +08:00

87 lines
2.8 KiB
TypeScript

/**
* 滚动相关 hooks —— 统一全站的滚动监听。
*
* 背景:此前滚动监听散落三处,各写一遍 addEventListener /
* removeEventListener / passive / 初始调用:
* · components/BackTop.tsx —— 监听 window,判断是否显示回到顶部
* · pages/Matches.tsx —— 监听联赛 tab 容器,判断能否右滚
* · admin/AdminLayout.tsx —— 关闭移动端抽屉时锁滚动
*
* 三份重复的生命周期管理,任何一处漏了 cleanup 就是内存泄漏;
* 且 `{ passive: true }` 这个纯收益的优化只有两处记得加。
*
* 这里抽成两个 hook,把「监听 → 清理」的模式收敛为单一实现。
*/
import { useEffect, useState } from 'react'
import type { RefObject } from 'react'
/**
* 监听 window 纵向滚动位置。
*
* @returns 当前 scrollY(节流到动画帧,避免滚动过程中高频 setState)
*/
export function useWindowScrollY(): number {
const [y, setY] = useState(() =>
typeof window === 'undefined' ? 0 : window.scrollY,
)
useEffect(() => {
let ticking = false
const onScroll = () => {
// rAF 节流:滚动事件可能每帧触发多次,直接 setState 会造成
// 大量无意义的重渲染
if (ticking) return
ticking = true
requestAnimationFrame(() => {
setY(window.scrollY)
ticking = false
})
}
window.addEventListener('scroll', onScroll, { passive: true })
onScroll() // 初始同步:刷新后可能已在页面中部
return () => window.removeEventListener('scroll', onScroll)
}, [])
return y
}
/**
* 监听某个可滚动元素是否还能继续向右滚动。
*
* 用于横向溢出的导航条:能右滚时在右缘显示渐隐提示,提示用户
* 「右边还有内容」。同时监听 window resize —— 视口变宽后可能
* 就不再溢出了,不重新计算会留下错误的提示。
*
* @param ref 目标滚动容器
* @param tolerance 容差(px),避免亚像素误差导致提示闪烁
* @param deps 触发重新测量的依赖(如列表内容变化时)
*/
export function useCanScrollRight(
ref: RefObject<HTMLElement | null>,
tolerance = 8,
deps: unknown[] = [],
): boolean {
const [canScroll, setCanScroll] = useState(false)
useEffect(() => {
const el = ref.current
if (!el) return
const update = () => {
setCanScroll(el.scrollWidth - el.scrollLeft - el.clientWidth > tolerance)
}
update() // 初次测量
el.addEventListener('scroll', update, { passive: true })
window.addEventListener('resize', update)
return () => {
el.removeEventListener('scroll', update)
window.removeEventListener('resize', update)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [ref, tolerance, ...deps])
return canScroll
}