网站 根目录 虚拟目录开发网站建设公司
本节将详细介绍如何使用 Tailwind CSS 进行移动端适配,包括响应式设计、触摸交互优化、性能优化等方面。
基础配置
视口配置
<!-- public/index.html -->
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover"><!-- 适配刘海屏 -->
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
断点设置
// tailwind.config.js
module.exports = {theme: {screens: {'xs': '375px','sm': '640px','md': '768px','lg': '1024px','xl': '1280px',// 自定义断点'mobile': '480px','tablet': '768px','laptop': '1024px','desktop': '1280px',},extend: {spacing: {'safe-top': 'env(safe-area-inset-top)','safe-bottom': 'env(safe-area-inset-bottom)','safe-left': 'env(safe-area-inset-left)','safe-right': 'env(safe-area-inset-right)',},},},
}
移动端导航
响应式导航组件
// components/MobileNav.tsx
import { useState, useEffect } from 'react';const MobileNav = () => {const [isOpen, setIsOpen] = useState(false);const [scrolled, setScrolled] = useState(false);useEffect(() => {const handleScroll = () => {setScrolled(window.scrollY > 20);};window.addEventListener('scroll', handleScroll);return () => window.removeEventListener('scroll', handleScroll);}, []);return (<>{/* 固定导航栏 */}<nav className={`fixed top-0 left-0 right-0 z-50transition-colors duration-200pt-safe-top${scrolled ? 'bg-white shadow-md' : 'bg-transparent'}`}><div className="px-4 py-3"><div className="flex items-center justify-between">{/* Logo */}<div className="flex-shrink-0"><imgclassName="h-8 w-auto"src="/logo.svg"alt="Logo"/></div>{/* 菜单按钮 */}<buttononClick={() => setIsOpen(!isOpen)}className="inline-flex items-center justify-center p-2 rounded-md text-gray-700 hover:text-gray-900 hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-blue-500"><span className="sr-only">打开菜单</span><svgclassName={`${isOpen ? 'hidden' : 'block'} h-6 w-6`}fill="none"viewBox="0 0 24 24"stroke="currentColor"><pathstrokeLinecap="round"strokeLinejoin="round"strokeWidth={2}d="M4 6h16M4 12h16M4 18h16"/></svg><svgclassName={`${isOpen ? 'block' : 'hidden'} h-6 w-6`}fill="none"viewBox="0 0 24 24"stroke="currentColor"><pathstrokeLinecap="round"strokeLinejoin="round"strokeWidth={2}d="M6 18L18 6M6 6l12 12"/></svg></button></div></div>{/* 移动端菜单 */}<divclassName={`fixed inset-0 bg-gray-900 bg-opacity-50 transition-opacity duration-300${isOpen ? 'opacity-100' : 'opacity-0 pointer-events-none'}`}onClick={() => setIsOpen(false)}><divclassName={`fixed inset-y-0 right-0 max-w-xs w-full bg-white shadow-xltransform transition-transform duration-300 ease-in-out${isOpen ? 'translate-x-0' : 'translate-x-full'}`}onClick={e => e.stopPropagation()}><div className="h-full flex flex-col">{/* 菜单头部 */}<div className="px-4 py-6 bg-gray-50"><div className="flex items-center justify-between"><h2 className="text-lg font-medium text-gray-900">菜单</h2><buttononClick={() => setIsOpen(false)}className="text-gray-500 hover:text-gray-700"><span className="sr-only">关闭菜单</span><svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" /></svg></button></div></div>{/* 菜单内容 */}<div className="flex-1 overflow-y-auto"><nav className="px-4 py-2"><div className="space-y-1"><ahref="#"className="block px-3 py-2 rounded-md text-base font-medium text-gray-900 hover:bg-gray-50">首页</a><ahref="#"className="block px-3 py-2 rounded-md text-base font-medium text-gray-900 hover:bg-gray-50">产品</a><ahref="#"className="block px-3 py-2 rounded-md text-base font-medium text-gray-900 hover:bg-gray-50">关于</a></div></nav></div></div></div></div></nav>{/* 占位元素,防止内容被固定导航栏遮挡 */}<div className="h-[calc(env(safe-area-inset-top)+3.5rem)]" /></>);
};
触摸交互优化
可触摸按钮组件
// components/TouchableButton.tsx
interface TouchableButtonProps {onPress?: () => void;className?: string;children: React.ReactNode;disabled?: boolean;
}const TouchableButton: React.FC<TouchableButtonProps> = ({onPress,className = '',children,disabled = false
}) => {return (<buttononClick={onPress}disabled={disabled}className={`relative overflow-hiddenactive:opacity-70transition-opacitytouch-manipulationselect-none${disabled ? 'opacity-50 cursor-not-allowed' : ''}${className}`}style={{WebkitTapHighlightColor: 'transparent',WebkitTouchCallout: 'none'}}>{children}{/* 触摸反馈效果 */}<div className="absolute inset-0 bg-black pointer-events-none opacity-0 active:opacity-10 transition-opacity" /></button>);
};
滑动列表组件
// components/SwipeableList.tsx
import { useState, useRef } from 'react';interface SwipeableListProps<T> {items: T[];renderItem: (item: T) => React.ReactNode;onSwipeLeft?: (item: T) => void;onSwipeRight?: (item: T) => void;
}function SwipeableList<T>({items,renderItem,onSwipeLeft,onSwipeRight
}: SwipeableListProps<T>) {const [activeIndex, setActiveIndex] = useState<number | null>(null);const touchStartX = useRef<number>(0);const currentOffset = useRef<number>(0);const handleTouchStart = (e: React.TouchEvent, index: number) => {touchStartX.current = e.touches[0].clientX;setActiveIndex(index);};const handleTouchMove = (e: React.TouchEvent) => {if (activeIndex === null) return;const touchX = e.touches[0].clientX;const diff = touchX - touchStartX.current;currentOffset.current = diff;// 更新滑动位置const element = e.currentTarget as HTMLElement;element.style.transform = `translateX(${diff}px)`;};const handleTouchEnd = (e: React.TouchEvent, item: T) => {if (activeIndex === null) return;const element = e.currentTarget as HTMLElement;const offset = currentOffset.current;// 判断滑动方向和距离if (Math.abs(offset) > 100) {if (offset > 0 && onSwipeRight) {onSwipeRight(item);} else if (offset < 0 && onSwipeLeft) {onSwipeLeft(item);}}// 重置状态element.style.transform = '';setActiveIndex(null);currentOffset.current = 0;};return (<div className="overflow-hidden">{items.map((item, index) => (<divkey={index}className="transform transition-transform touch-pan-y"onTouchStart={e => handleTouchStart(e, index)}onTouchMove={handleTouchMove}onTouchEnd={e => handleTouchEnd(e, item)}>{renderItem(item)}</div>))}</div>);
}
性能优化
图片优化
// components/OptimizedImage.tsx
interface OptimizedImageProps {src: string;alt: string;sizes?: string;className?: string;
}const OptimizedImage: React.FC<OptimizedImageProps> = ({src,alt,sizes = '100vw',className = ''
}) => {return (<picture><sourcemedia="(min-width: 1024px)"srcSet={`${src}?w=1024 1024w, ${src}?w=1280 1280w`}sizes={sizes}/><sourcemedia="(min-width: 768px)"srcSet={`${src}?w=768 768w, ${src}?w=1024 1024w`}sizes={sizes}/><imgsrc={`${src}?w=375`}srcSet={`${src}?w=375 375w, ${src}?w=640 640w`}sizes={sizes}alt={alt}className={`w-full h-auto ${className}`}loading="lazy"decoding="async"/></picture>);
};
虚拟列表
// components/VirtualList.tsx
import { useState, useEffect, useRef } from 'react';interface VirtualListProps<T> {items: T[];renderItem: (item: T) => React.ReactNode;itemHeight: number;containerHeight: number;overscan?: number;
}function VirtualList<T>({items,renderItem,itemHeight,containerHeight,overscan = 3
}: VirtualListProps<T>) {const [scrollTop, setScrollTop] = useState(0);const containerRef = useRef<HTMLDivElement>(null);// 计算可见范围const visibleCount = Math.ceil(containerHeight / itemHeight);const totalHeight = items.length * itemHeight;const startIndex = Math.max(0, Math.floor(scrollTop / itemHeight) - overscan);const endIndex = Math.min(items.length,Math.ceil((scrollTop + containerHeight) / itemHeight) + overscan);// 可见项目const visibleItems = items.slice(startIndex, endIndex);const handleScroll = () => {if (containerRef.current) {setScrollTop(containerRef.current.scrollTop);}};return (<divref={containerRef}className="overflow-auto"style={{ height: containerHeight }}onScroll={handleScroll}><div style={{ height: totalHeight, position: 'relative' }}>{visibleItems.map((item, index) => (<divkey={startIndex + index}style={{position: 'absolute',top: (startIndex + index) * itemHeight,height: itemHeight,width: '100%'}}>{renderItem(item)}</div>))}</div></div>);
}
手势交互
滑动手势处理
// hooks/useSwipe.ts
interface SwipeOptions {onSwipeLeft?: () => void;onSwipeRight?: () => void;onSwipeUp?: () => void;onSwipeDown?: () => void;threshold?: number;
}export const useSwipe = (options: SwipeOptions = {}) => {const {onSwipeLeft,onSwipeRight,onSwipeUp,onSwipeDown,threshold = 50} = options;const touchStart = useRef({ x: 0, y: 0 });const touchEnd = useRef({ x: 0, y: 0 });const handleTouchStart = (e: TouchEvent) => {touchStart.current = {x: e.touches[0].clientX,y: e.touches[0].clientY};};const handleTouchEnd = (e: TouchEvent) => {touchEnd.current = {x: e.changedTouches[0].clientX,y: e.changedTouches[0].clientY};const deltaX = touchEnd.current.x - touchStart.current.x;const deltaY = touchEnd.current.y - touchStart.current.y;if (Math.abs(deltaX) > Math.abs(deltaY)) {// 水平滑动if (Math.abs(deltaX) > threshold) {if (deltaX > 0) {onSwipeRight?.();} else {onSwipeLeft?.();}}} else {// 垂直滑动if (Math.abs(deltaY) > threshold) {if (deltaY > 0) {onSwipeDown?.();} else {onSwipeUp?.();}}}};return {handleTouchStart,handleTouchEnd};
};
下拉刷新组件
// components/PullToRefresh.tsx
interface PullToRefreshProps {onRefresh: () => Promise<void>;children: React.ReactNode;
}const PullToRefresh: React.FC<PullToRefreshProps> = ({onRefresh,children
}) => {const [refreshing, setRefreshing] = useState(false);const [pullDistance, setPullDistance] = useState(0);const containerRef = useRef<HTMLDivElement>(null);const touchStart = useRef(0);const pulling = useRef(false);const handleTouchStart = (e: React.TouchEvent) => {if (containerRef.current?.scrollTop === 0) {touchStart.current = e.touches[0].clientY;pulling.current = true;}};const handleTouchMove = (e: React.TouchEvent) => {if (!pulling.current) return;const touch = e.touches[0].clientY;const distance = touch - touchStart.current;if (distance > 0) {e.preventDefault();setPullDistance(Math.min(distance * 0.5, 100));}};const handleTouchEnd = async () => {if (!pulling.current) return;pulling.current = false;if (pullDistance > 60 && !refreshing) {setRefreshing(true);try {await onRefresh();} finally {setRefreshing(false);}}setPullDistance(0);};return (<divref={containerRef}className="overflow-auto touch-pan-y"onTouchStart={handleTouchStart}onTouchMove={handleTouchMove}onTouchEnd={handleTouchEnd}>{/* 刷新指示器 */}<divclassName="flex items-center justify-center transition-transform"style={{transform: `translateY(${pullDistance}px)`,height: refreshing ? '50px' : '0'}}>{refreshing ? (<div className="animate-spin rounded-full h-6 w-6 border-2 border-gray-900 border-t-transparent" />) : (<div className="h-6 w-6 transition-transform" style={{transform: `rotate(${Math.min(pullDistance * 3.6, 360)}deg)`}}>↓</div>)}</div>{/* 内容区域 */}<div style={{transform: `translateY(${pullDistance}px)`,transition: pulling.current ? 'none' : 'transform 0.2s'}}>{children}</div></div>);
};
自适应布局
媒体查询工具
// hooks/useMediaQuery.ts
export const useMediaQuery = (query: string) => {const [matches, setMatches] = useState(false);useEffect(() => {const media = window.matchMedia(query);const updateMatch = (e: MediaQueryListEvent) => {setMatches(e.matches);};setMatches(media.matches);media.addListener(updateMatch);return () => media.removeListener(updateMatch);}, [query]);return matches;
};// 使用示例
const isMobile = useMediaQuery('(max-width: 768px)');
const isTablet = useMediaQuery('(min-width: 769px) and (max-width: 1024px)');
const isDesktop = useMediaQuery('(min-width: 1025px)');
自适应容器
// components/AdaptiveContainer.tsx
interface AdaptiveContainerProps {children: React.ReactNode;className?: string;
}const AdaptiveContainer: React.FC<AdaptiveContainerProps> = ({children,className = ''
}) => {return (<div className={`w-full px-4 mx-autosm:max-w-screen-sm sm:px-6md:max-w-screen-mdlg:max-w-screen-lg lg:px-8xl:max-w-screen-xl${className}`}>{children}</div>);
};
调试工具
设备模拟器
// components/DeviceEmulator.tsx
interface DeviceEmulatorProps {children: React.ReactNode;device?: 'iphone' | 'ipad' | 'android' | 'pixel';
}const deviceSpecs = {iphone: {width: '375px',height: '812px',safeAreaTop: '44px',safeAreaBottom: '34px'},ipad: {width: '768px',height: '1024px',safeAreaTop: '20px',safeAreaBottom: '20px'},// ... 其他设备规格
};const DeviceEmulator: React.FC<DeviceEmulatorProps> = ({children,device = 'iphone'
}) => {const specs = deviceSpecs[device];return (<divclassName="relative bg-black rounded-[3rem] p-4"style={{width: `calc(${specs.width} + 2rem)`,height: `calc(${specs.height} + 2rem)`}}><divclassName="overflow-hidden rounded-[2.5rem] bg-white"style={{width: specs.width,height: specs.height,paddingTop: specs.safeAreaTop,paddingBottom: specs.safeAreaBottom}}>{children}</div></div>);
};
开发者工具
// utils/mobileDebugger.ts
export const initMobileDebugger = () => {if (process.env.NODE_ENV === 'development') {// 显示点击区域document.addEventListener('touchstart', (e) => {const touch = e.touches[0];const dot = document.createElement('div');dot.style.cssText = `position: fixed;z-index: 9999;width: 20px;height: 20px;background: rgba(255, 0, 0, 0.5);border-radius: 50%;pointer-events: none;transform: translate(-50%, -50%);left: ${touch.clientX}px;top: ${touch.clientY}px;`;document.body.appendChild(dot);setTimeout(() => dot.remove(), 500);});// 显示视口信息const viewport = document.createElement('div');viewport.style.cssText = `position: fixed;z-index: 9999;bottom: 0;left: 0;background: rgba(0, 0, 0, 0.7);color: white;padding: 4px 8px;font-size: 12px;`;document.body.appendChild(viewport);const updateViewport = () => {viewport.textContent = `${window.innerWidth}x${window.innerHeight}`;};window.addEventListener('resize', updateViewport);updateViewport();}
};
最佳实践
-
响应式设计
- 移动优先策略
- 合理的断点设置
- 灵活的布局系统
-
触摸交互
- 适当的点击区域
- 清晰的反馈效果
- 流畅的动画过渡
-
性能优化
- 图片优化处理
- 延迟加载策略
- 虚拟滚动列表
-
用户体验
- 合理的字体大小
- 清晰的视觉层级
- 直观的操作反馈