feat: 完成前端响应式设计优化,增强移动端体验

- 添加触摸手势支持库 (react-spring + @use-gesture/react)
- 照片模态框增加左右滑动切换功能
- 照片画廊增加下拉刷新功能 (移动端)
- 优化所有按钮符合44px最小触摸目标标准
- 增强移动端导航体验,增加悬停和选中状态
- 创建设备信息检测钩子 (useDeviceInfo)
- 开发优化图片组件,支持懒加载和骨架屏
- 改进移动端手势交互和视觉反馈
- 完善响应式断点系统和触摸设备检测
- 前端构建测试成功,开发服务器正常启动

Task 23 completed: 前端响应式设计优化
This commit is contained in:
xujiang
2025-07-11 12:40:46 +08:00
parent 9046befcf1
commit 494d98bee5
9 changed files with 946 additions and 28 deletions

View File

@ -0,0 +1,140 @@
"use client"
import { useState, useRef, useEffect } from "react"
import Image from "next/image"
import { useDeviceInfo } from "@/hooks/use-mobile"
interface OptimizedImageProps {
src: string
alt: string
width?: number
height?: number
className?: string
priority?: boolean
onClick?: () => void
onLoad?: () => void
}
export function OptimizedImage({
src,
alt,
width,
height,
className = "",
priority = false,
onClick,
onLoad
}: OptimizedImageProps) {
const [isLoaded, setIsLoaded] = useState(false)
const [isInView, setIsInView] = useState(false)
const [hasError, setHasError] = useState(false)
const imgRef = useRef<HTMLDivElement>(null)
const { isMobile, viewport } = useDeviceInfo()
// 交叉观察器用于懒加载
useEffect(() => {
if (priority) {
setIsInView(true)
return
}
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
setIsInView(true)
observer.disconnect()
}
},
{
rootMargin: isMobile ? "50px" : "100px", // 移动端更小的预加载距离
threshold: 0.1
}
)
if (imgRef.current) {
observer.observe(imgRef.current)
}
return () => observer.disconnect()
}, [priority, isMobile])
const handleLoad = () => {
setIsLoaded(true)
onLoad?.()
}
const handleError = () => {
setHasError(true)
}
// 根据设备选择合适的图片质量
const getOptimizedSrc = (originalSrc: string) => {
if (hasError) return "/placeholder.svg"
// 如果是移动端,可以考虑使用更低质量的图片
if (isMobile && viewport.width < 768) {
// 这里可以添加图片质量优化逻辑
// 例如:添加 ?quality=80&w=400 等参数
return originalSrc
}
return originalSrc
}
return (
<div
ref={imgRef}
className={`relative overflow-hidden ${className}`}
onClick={onClick}
>
{/* 骨架屏加载状态 */}
{!isLoaded && isInView && (
<div className="absolute inset-0 bg-gray-200 animate-pulse">
<div className="flex items-center justify-center h-full">
<div className="w-8 h-8 border-2 border-gray-300 border-t-gray-600 rounded-full animate-spin"></div>
</div>
</div>
)}
{/* 错误状态 */}
{hasError && (
<div className="absolute inset-0 bg-gray-100 flex items-center justify-center">
<div className="text-center text-gray-500">
<svg className="w-12 h-12 mx-auto mb-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
<p className="text-xs"></p>
</div>
</div>
)}
{/* 实际图片 */}
{isInView && !hasError && (
<Image
src={getOptimizedSrc(src)}
alt={alt}
width={width}
height={height}
fill={!width || !height}
className={`object-cover transition-opacity duration-500 ${
isLoaded ? "opacity-100" : "opacity-0"
}`}
onLoad={handleLoad}
onError={handleError}
priority={priority}
draggable={false}
sizes={
isMobile
? "(max-width: 768px) 100vw, 50vw"
: "(max-width: 1024px) 50vw, 33vw"
}
/>
)}
{/* 渐进式加载效果 */}
{isLoaded && (
<div className={`absolute inset-0 bg-gradient-to-t from-black/20 via-transparent to-transparent opacity-0 transition-opacity duration-300 ${onClick ? "group-hover:opacity-100" : ""}`} />
)}
</div>
)
}