import { h } from '@ui-arch/frame'; import type { BackTopProps } from './types.js'; /*** 返回顶部:滚动越过阈值浮现,环形进度显示已滚比例,点击平滑回顶(尊重减少动效偏好) ***/ export function backTop(props: BackTopProps = {}): HTMLElement { // —— 状态集中声明 —— const threshold = props.threshold ?? 300; // 浮现阈值 const scroller = props.target ?? window; // 滚动容器 const el = props.target instanceof HTMLElement ? props.target : null; // 元素容器:null 表示整页 const ring = h('span', { class: 'ua-backtop-ring', 'aria-hidden': 'true' }); // 进度环(纯装饰) const btn = h( 'button', { type: 'button', class: 'ua-backtop', 'aria-label': '回到顶部', onClick: () => jump() }, ring, h('span', { class: 'ua-backtop-icon', 'aria-hidden': 'true' }, '↑'), ) as HTMLButtonElement; /*** 当前滚动位置:整页用 window.scrollY,容器用 scrollTop ***/ const offset = (): number => (el ? el.scrollTop : window.scrollY); /*** 可滚动总距离:不足 0 时按 0 处理,避免除零与负比例 ***/ const distance = (): number => (el ? el.scrollHeight - el.clientHeight : document.documentElement.scrollHeight - window.innerHeight) || 0; /*** 同步浮现态与进度环:仅在阈值上下切换时才改类名,减少样式重算 ***/ const sync = (): void => { const y = offset(); const total = distance(); btn.classList.toggle('is-visible', y > threshold); // 越过阈值才浮现 if (props.progress === false) return; // 卫语句:关掉进度环就不再算角度 const deg = total > 0 ? Math.min(Math.round((y / total) * 360), 360) : 0; ring.style.background = `conic-gradient(var(--ua-color-accent-contrast) ${deg}deg, transparent 0)`; // 已滚比例用纯色弧 }; /*** 回顶:平滑滚动,用户偏好减少动效时直接跳转 ***/ const jump = (): void => { const smooth = !window.matchMedia('(prefers-reduced-motion: reduce)').matches; if (el) el.scrollTo({ top: 0, behavior: smooth ? 'smooth' : 'auto' }); else window.scrollTo({ top: 0, behavior: smooth ? 'smooth' : 'auto' }); props.onClick?.(); }; scroller.addEventListener('scroll', sync, { passive: true }); // 被动监听:不阻塞滚动 window.addEventListener('resize', sync); // 视口变化会改可滚距离 sync(); // 首屏同步一次,避免刷新后停在中间位置却未浮现 return btn; }