'use client'; import { useRef, useCallback, useEffect, useState } from 'react'; import { useRouter } from 'next/navigation'; import { motion, AnimatePresence } from 'framer-motion'; import { ArrowLeft, ArrowRight } from 'lucide-react'; import { cn } from '@/lib/utils'; interface SwipeGestureOptions { threshold?: number; edgeSize?: number; enabled?: boolean; onSwipeLeft?: () => void; onSwipeRight?: () => void; } interface SwipeState { isSwiping: boolean; startX: number; currentX: number; progress: number; direction: 'left' | 'right' | null; } export function useSwipeGesture(options: SwipeGestureOptions = {}) { const { edgeSize = 30, enabled = true, onSwipeLeft, onSwipeRight, } = options; const swipeState = useRef({ isSwiping: false, startX: 0, currentX: 0, progress: 0, direction: null, }); const containerRef = useRef(null); const handleTouchStart = useCallback((e: Event) => { if (!enabled) return; const touchEvent = e as TouchEvent; const touch = touchEvent.touches?.[0]; if (!touch) return; const x = touch.clientX; if (x > window.innerWidth - edgeSize || x < edgeSize) { swipeState.current = { isSwiping: true, startX: x, currentX: x, progress: 0, direction: null, }; } }, [enabled, edgeSize]); const handleTouchMove = useCallback((e: Event) => { if (!swipeState.current.isSwiping) return; e.preventDefault(); const touchEvent = e as TouchEvent; const touch = touchEvent.touches?.[0]; if (!touch) return; swipeState.current.currentX = touch.clientX; const deltaX = swipeState.current.currentX - swipeState.current.startX; const absDeltaX = Math.abs(deltaX); if (absDeltaX > 10) { swipeState.current.direction = deltaX > 0 ? 'right' : 'left'; swipeState.current.progress = Math.min(absDeltaX / (window.innerWidth * 0.35), 1); } }, []); const handleTouchEnd = useCallback(() => { if (!swipeState.current.isSwiping) return; const { progress, direction } = swipeState.current; if (progress > 0.3 && direction) { if (direction === 'left') { triggerHaptic('light'); onSwipeLeft?.(); } else { triggerHaptic('light'); onSwipeRight?.(); } } swipeState.current = { isSwiping: false, startX: 0, currentX: 0, progress: 0, direction: null, }; }, [onSwipeLeft, onSwipeRight]); useEffect(() => { const element = containerRef.current || document; element.addEventListener('touchstart', handleTouchStart, { passive: true }); element.addEventListener('touchmove', handleTouchMove, { passive: false }); element.addEventListener('touchend', handleTouchEnd, { passive: true }); return () => { element.removeEventListener('touchstart', handleTouchStart); element.removeEventListener('touchmove', handleTouchMove); element.removeEventListener('touchend', handleTouchEnd); }; }, [handleTouchStart, handleTouchMove, handleTouchEnd]); return { containerRef, swipeState }; } function triggerHaptic(style: 'light' | 'medium' | 'heavy' = 'light') { if ('vibrate' in navigator) { switch (style) { case 'light': navigator.vibrate(10); break; case 'medium': navigator.vibrate(25); break; case 'heavy': navigator.vibrate(50); break; } } } interface SwipeNavigationProps { prevRoute?: string; nextRoute?: string; prevLabel?: string; nextLabel?: string; className?: string; } export function SwipeNavigation({ prevRoute, nextRoute, prevLabel = '上一页', nextLabel = '下一页', className, }: SwipeNavigationProps) { const router = useRouter(); const [swipeHint, setSwipeHint] = useState<'prev' | 'next' | null>(null); const [showHint, setShowHint] = useState(false); const { containerRef } = useSwipeGesture({ onSwipeLeft: () => { if (nextRoute) { setSwipeHint('next'); setTimeout(() => router.push(nextRoute), 200); } }, onSwipeRight: () => { if (prevRoute) { setSwipeHint('prev'); setTimeout(() => router.push(prevRoute), 200); } }, }); useEffect(() => { const hasVisited = sessionStorage.getItem('swipe-hint-shown'); if (!hasVisited && (prevRoute || nextRoute)) { const timer = setTimeout(() => setShowHint(true), 1500); sessionStorage.setItem('swipe-hint-shown', 'true'); return () => clearTimeout(timer); } return undefined; }, [prevRoute, nextRoute]); useEffect(() => { if (showHint) { const timer = setTimeout(() => setShowHint(false), 3000); return () => clearTimeout(timer); } return undefined; }, [showHint]); return ( <>
} className={cn(className)} /> {swipeHint && ( {swipeHint === 'prev' ? ( <> {prevLabel} ) : ( <> {nextLabel} )} )} {showHint && (

提示

尝试左右滑动屏幕切换页面

{prevRoute && (
{prevLabel}
)} {nextRoute && (
{nextLabel}
)}
)}
{(prevRoute || nextRoute) && (
)} ); } interface PullToRefreshProps { onRefresh: () => Promise; children: React.ReactNode; className?: string; } export function PullToRefresh({ onRefresh, children, className }: PullToRefreshProps) { const [pullDistance, setPullDistance] = useState(0); const [isRefreshing, setIsRefreshing] = useState(false); const startY = useRef(0); const isPulling = useRef(false); const handleTouchStart = (e: React.TouchEvent) => { if (window.scrollY === 0 && e.touches?.[0]) { startY.current = e.touches[0].clientY; isPulling.current = true; } }; const handleTouchMove = (e: React.TouchEvent) => { if (!isPulling.current || isRefreshing) return; const touch = e.touches?.[0]; if (!touch) return; const currentY = touch.clientY; const distance = Math.max(0, (currentY - startY.current) * 0.5); if (distance > 0 && window.scrollY === 0) { e.preventDefault(); setPullDistance(Math.min(distance, 100)); } }; const handleTouchEnd = async () => { if (!isPulling.current) return; isPulling.current = false; if (pullDistance > 60 && !isRefreshing) { setIsRefreshing(true); triggerHaptic('medium'); try { await onRefresh(); } finally { setIsRefreshing(false); } } setPullDistance(0); }; return (
60 ? 180 : 0 }} transition={{ duration: 0.3 }} > 60 ? 'text-[var(--color-brand)]' : 'text-[var(--color-text-subtle)]' )} fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2} >
{children}
); }