txw/txw-mhzc-web/src/pages/index/utils/portal-scroll-mode.js
2026-06-02 23:28:48 +08:00

123 lines
4.4 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 门户滚动工具集
*
* 设计原则(参考 Material Design 3 / iOS Safari 滚动模型 / Nuxt 3 page transition
* 1. **绝不劫持 wheel 事件** —— 浏览器原生滚动在 GPU 合成、滚动惯性、
* 触控板缩放、辅助滚动、屏幕阅读器等场景下都是最优的。手写 wheel handler
* 累加 deltaY + setTimeout 锁会导致"滑动不稳 / 向下滑却向上滚"。
* 2. **分屏落点用 scrollIntoView + scrollend 判定** —— 让浏览器原生动画,
* 只在 scrollend 时解锁 / 计算下一段。
* 3. **CSS overscroll-behavior: contain 隔离滚动冒泡** —— 子容器滚动到边界
* 时不再触发父级连锁滚动。
* 4. **deltaMode 区分鼠标/触控板** —— 阈值判断用 pixel 模式。
*/
const SCROLL_END_TIMEOUT_MS = 120;
/**
* 桌面端全屏分屏滚轮(兼容旧调用方;新实现改为原生滚动 + scrollIntoView
* 现仅保留接口签名供外部 import不主动 bind wheel。
*/
export function shouldUseSectionWheelScroll() {
if (typeof window === 'undefined') return true;
return !window.matchMedia('(max-width: 767px), (pointer: coarse)').matches;
}
/**
* @deprecated 老接口:不再劫持 wheel 事件。新逻辑请直接用 scrollIntoView + scrollend。
* 此函数保留仅为不破坏外部 import运行时不绑定任何监听。
*/
export function bindSectionWheelScroll(root, handler) {
// 静默 no-op提示调用方迁移
if (typeof console !== 'undefined') {
// eslint-disable-next-line no-console
console.warn(
'[portal-scroll-mode] bindSectionWheelScroll 已废弃:不再劫持 wheel 事件。'
+ ' 请改用 scrollIntoView({ behavior: "smooth" }) + scrollend 判定。',
);
}
return () => {};
}
/**
* 门户主内容区滚回顶部(分页、筛选等页内操作)
* @param {{ smooth?: boolean, selector?: string }} [options]
*/
export function scrollPortalContentToTop(options = {}) {
if (typeof window === 'undefined') return;
const { smooth = true, selector = '.content-wrap' } = options;
const root = document.querySelector(selector);
const behavior = smooth ? 'smooth' : 'auto';
if (root) {
root.scrollTo({ top: 0, behavior });
return;
}
window.scrollTo({ top: 0, behavior });
}
/**
* Promise 化的 scrollend —— 等到滚动真正结束再 resolve。
* 兼容不支持 scrollend 的浏览器Safari < 15.4fallback 到 debounced scroll。
* @param {HTMLElement | Window} target
* @param {number} timeoutMs
* @returns {Promise<void>}
*/
export function waitForScrollEnd(target = window, timeoutMs = SCROLL_END_TIMEOUT_MS) {
return new Promise((resolve) => {
if (typeof window === 'undefined') {
resolve();
return;
}
let settled = false;
const done = () => {
if (settled) return;
settled = true;
target.removeEventListener && target.removeEventListener('scrollend', done);
target.removeEventListener && target.removeEventListener('scroll', onScroll);
resolve();
};
const onScroll = () => {
if (settled) return;
if (debounceTimer) clearTimeout(debounceTimer);
debounceTimer = setTimeout(done, timeoutMs);
};
let debounceTimer = setTimeout(done, timeoutMs);
if ('onscrollend' in window) {
target.addEventListener('scrollend', done, { once: true, passive: true });
} else {
target.addEventListener('scroll', onScroll, { passive: true });
}
});
}
/**
* 平滑滚动到指定 section 元素,等待滚动结束。
* @param {HTMLElement} el 目标元素
* @param {{ block?: ScrollLogicalPosition, offset?: number, scrollRoot?: HTMLElement | Window }} [options]
* @returns {Promise<void>}
*/
export async function smoothScrollTo(el, options = {}) {
if (!el) return;
const { block = 'start', offset = 0, scrollRoot = window } = options;
const behavior = 'smooth';
// 处理 offsetscrollIntoView 不支持 offset临时用 transform 占位再回滚
if (offset !== 0) {
const orig = el.style.scrollMarginTop;
el.style.scrollMarginTop = `${offset}px`;
try {
el.scrollIntoView({ behavior, block, inline: 'nearest' });
} finally {
// 还原nextTick 后给浏览器时间生效)
setTimeout(() => {
el.style.scrollMarginTop = orig;
}, 0);
}
} else {
el.scrollIntoView({ behavior, block, inline: 'nearest' });
}
await waitForScrollEnd(scrollRoot);
}