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