/** * 下拉刷新通用工具 (BEXPullRefresh) * * 用法: * var ptr = BEXPullRefresh.init(document.getElementById('myContainer'), { * onRefresh: async function () { await fetchData(); }, * texts: { pull: '下拉刷新', release: '释放刷新', refreshing: '刷新中...' }, * threshold: 56 * }); * * // 手动销毁:ptr.destroy(); * // 销毁全部:BEXPullRefresh.destroyAll(); */ (function () { 'use strict'; var instances = []; function PTRInstance(container, options) { var opts = options || {}; var threshold = opts.threshold || 56; var onRefresh = opts.onRefresh || function () { return Promise.resolve(); }; var texts = opts.texts || { pull: 'Pull to refresh', release: 'Release to refresh', refreshing: 'Refreshing...' }; var state = { pulling: false, refreshing: false, startY: 0, currentY: 0 }; var indicatorEl = null; var spinnerEl = null; var textEl = null; function buildDOM() { indicatorEl = document.createElement('div'); indicatorEl.className = 'ptr-indicator'; indicatorEl.innerHTML = '
' + '' + texts.pull + ''; var parent = container.parentNode; parent.insertBefore(indicatorEl, container); spinnerEl = indicatorEl.querySelector('.ptr-spinner'); textEl = indicatorEl.querySelector('.ptr-text'); container.classList.add('ptr-container'); // 确保父级支持 absolute 定位 if (parent !== document.body) { var parentPos = window.getComputedStyle(parent).position; if (parentPos === 'static') parent.style.position = 'relative'; } } function getScrollTop() { return document.documentElement.scrollTop || document.body.scrollTop || 0; } function applyTransform(y, noTransition) { if (noTransition) container.classList.add('ptr-pulling'); else container.classList.remove('ptr-pulling'); container.style.transform = 'translateY(' + y + 'px)'; // 指示器从 translateY(-100%) → 0%:y 0→threshold 时,百分比 -100→0 var pullPercent = ((y / threshold) * 100 - 100).toFixed(1); indicatorEl.style.transform = 'translateY(' + pullPercent + '%)'; } function setSpinnerOpacity(opacity) { spinnerEl.style.opacity = String(opacity); } function startRefresh() { state.refreshing = true; indicatorEl.classList.add('ptr-refreshing'); applyTransform(threshold, false); setSpinnerOpacity(1); textEl.textContent = texts.refreshing; onRefresh().then(function () { finishRefresh(); }).catch(function () { finishRefresh(); }); } function finishRefresh() { state.refreshing = false; indicatorEl.classList.remove('ptr-refreshing'); container.classList.remove('ptr-pulling'); container.style.transform = 'translateY(0)'; indicatorEl.style.transform = 'translateY(-100%)'; setSpinnerOpacity(0); textEl.textContent = texts.pull; } function onTouchStart(e) { if (state.refreshing) return; state.startY = e.touches[0].clientY; state.pulling = false; } function isDocumentScrollLocked() { var htmlStyle = window.getComputedStyle(document.documentElement); return htmlStyle.overflow === 'hidden' || htmlStyle.overflowY === 'hidden'; } function onTouchMove(e) { if (state.refreshing || isDocumentScrollLocked()) return; var scrollTop = getScrollTop(); if (scrollTop > 0) return; var deltaY = e.touches[0].clientY - state.startY; if (deltaY <= 0) return; state.pulling = true; state.currentY = Math.min(deltaY * 0.5, threshold * 1.5); applyTransform(state.currentY, true); setSpinnerOpacity(Math.min(state.currentY / threshold, 1)); textEl.textContent = state.currentY >= threshold ? texts.release : texts.pull; } function onTouchEnd() { if (!state.pulling || state.refreshing) return; state.pulling = false; if (state.currentY >= threshold) { startRefresh(); } else { applyTransform(0, false); setSpinnerOpacity(0); textEl.textContent = texts.pull; } } this.destroy = function () { document.removeEventListener('touchstart', onTouchStart); document.removeEventListener('touchmove', onTouchMove); document.removeEventListener('touchend', onTouchEnd); if (indicatorEl && indicatorEl.parentNode) { indicatorEl.parentNode.removeChild(indicatorEl); } container.classList.remove('ptr-container', 'ptr-pulling'); container.style.transform = ''; var idx = instances.indexOf(this); if (idx > -1) instances.splice(idx, 1); }; buildDOM(); document.addEventListener('touchstart', onTouchStart, { passive: true }); document.addEventListener('touchmove', onTouchMove, { passive: true }); document.addEventListener('touchend', onTouchEnd); instances.push(this); } window.BEXPullRefresh = { init: function (container, options) { if (!container) throw new Error('BEXPullRefresh.init: container is required'); return new PTRInstance(container, options); }, destroyAll: function () { while (instances.length) { instances[instances.length - 1].destroy(); } } }; })();