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