feat: 完成前端响应式设计优化,增强移动端体验
- 添加触摸手势支持库 (react-spring + @use-gesture/react) - 照片模态框增加左右滑动切换功能 - 照片画廊增加下拉刷新功能 (移动端) - 优化所有按钮符合44px最小触摸目标标准 - 增强移动端导航体验,增加悬停和选中状态 - 创建设备信息检测钩子 (useDeviceInfo) - 开发优化图片组件,支持懒加载和骨架屏 - 改进移动端手势交互和视觉反馈 - 完善响应式断点系统和触摸设备检测 - 前端构建测试成功,开发服务器正常启动 Task 23 completed: 前端响应式设计优化
This commit is contained in:
@ -1,11 +1,13 @@
|
||||
"use client"
|
||||
|
||||
import { useEffect } from "react"
|
||||
import { useEffect, useRef } from "react"
|
||||
import Image from "next/image"
|
||||
import { X, ChevronLeft, ChevronRight, Camera, MapPin, Calendar } from "lucide-react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Card, CardContent } from "@/components/ui/card"
|
||||
import { useDrag } from "@use-gesture/react"
|
||||
import { useSpring, animated } from "react-spring"
|
||||
|
||||
import { Photo } from '@/lib/queries'
|
||||
|
||||
@ -17,6 +19,41 @@ interface PhotoModalProps {
|
||||
}
|
||||
|
||||
export function PhotoModal({ photo, onClose, onPrev, onNext }: PhotoModalProps) {
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
// 触摸手势支持
|
||||
const [{ x }, api] = useSpring(() => ({ x: 0 }))
|
||||
|
||||
const bind = useDrag(
|
||||
({ down, movement: [mx], velocity: [vx], direction: [xDir], cancel }) => {
|
||||
// 如果滑动速度足够快或者滑动距离超过阈值,触发切换
|
||||
const trigger = Math.abs(mx) > window.innerWidth * 0.3 || Math.abs(vx) > 0.5
|
||||
|
||||
if (trigger && !down) {
|
||||
// 根据滑动方向决定前进或后退
|
||||
if (xDir > 0) {
|
||||
onPrev() // 向右滑动,显示上一张
|
||||
} else {
|
||||
onNext() // 向左滑动,显示下一张
|
||||
}
|
||||
cancel() // 取消手势
|
||||
}
|
||||
|
||||
// 设置拖拽动画
|
||||
api.start({
|
||||
x: down ? mx : 0,
|
||||
immediate: down,
|
||||
config: { tension: 300, friction: 30 }
|
||||
})
|
||||
},
|
||||
{
|
||||
axis: 'x',
|
||||
bounds: { left: -window.innerWidth, right: window.innerWidth },
|
||||
rubberband: true,
|
||||
filterTaps: true,
|
||||
}
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
switch (e.key) {
|
||||
@ -47,7 +84,7 @@ export function PhotoModal({ photo, onClose, onPrev, onNext }: PhotoModalProps)
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="absolute top-4 right-4 z-10 text-white hover:bg-white/10"
|
||||
className="absolute top-4 right-4 z-10 text-white hover:bg-white/10 min-h-[44px] min-w-[44px] p-2"
|
||||
onClick={onClose}
|
||||
>
|
||||
<X className="h-6 w-6" />
|
||||
@ -57,7 +94,7 @@ export function PhotoModal({ photo, onClose, onPrev, onNext }: PhotoModalProps)
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="absolute left-4 top-1/2 -translate-y-1/2 z-10 text-white hover:bg-white/10"
|
||||
className="absolute left-4 top-1/2 -translate-y-1/2 z-10 text-white hover:bg-white/10 min-h-[44px] min-w-[44px] p-2"
|
||||
onClick={onPrev}
|
||||
>
|
||||
<ChevronLeft className="h-8 w-8" />
|
||||
@ -66,7 +103,7 @@ export function PhotoModal({ photo, onClose, onPrev, onNext }: PhotoModalProps)
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="absolute right-4 top-1/2 -translate-y-1/2 z-10 text-white hover:bg-white/10"
|
||||
className="absolute right-4 top-1/2 -translate-y-1/2 z-10 text-white hover:bg-white/10 min-h-[44px] min-w-[44px] p-2"
|
||||
onClick={onNext}
|
||||
>
|
||||
<ChevronRight className="h-8 w-8" />
|
||||
@ -75,16 +112,22 @@ export function PhotoModal({ photo, onClose, onPrev, onNext }: PhotoModalProps)
|
||||
<div className="w-full max-w-7xl mx-auto grid lg:grid-cols-3 gap-6 h-full max-h-[90vh]">
|
||||
{/* Main image */}
|
||||
<div className="lg:col-span-2 relative flex items-center justify-center">
|
||||
<div className="relative w-full h-full max-h-[80vh] flex items-center justify-center">
|
||||
<animated.div
|
||||
ref={containerRef}
|
||||
{...bind()}
|
||||
style={{ x }}
|
||||
className="relative w-full h-full max-h-[80vh] flex items-center justify-center touch-pan-y cursor-grab active:cursor-grabbing"
|
||||
>
|
||||
<Image
|
||||
src={photo.src || "/placeholder.svg"}
|
||||
alt={photo.title}
|
||||
width={1200}
|
||||
height={800}
|
||||
className="max-w-full max-h-full object-contain"
|
||||
className="max-w-full max-h-full object-contain select-none"
|
||||
priority
|
||||
draggable={false}
|
||||
/>
|
||||
</div>
|
||||
</animated.div>
|
||||
</div>
|
||||
|
||||
{/* Photo information */}
|
||||
@ -144,12 +187,14 @@ export function PhotoModal({ photo, onClose, onPrev, onNext }: PhotoModalProps)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Keyboard shortcuts */}
|
||||
{/* Keyboard shortcuts and gestures */}
|
||||
<div className="pt-4 border-t border-gray-200">
|
||||
<h3 className="text-sm font-medium text-gray-900 mb-2">快捷键</h3>
|
||||
<h3 className="text-sm font-medium text-gray-900 mb-2">操作说明</h3>
|
||||
<div className="text-xs text-gray-500 space-y-1">
|
||||
<div>ESC - 关闭</div>
|
||||
<div>← → - 切换图片</div>
|
||||
<div className="md:hidden">左右滑动 - 切换图片</div>
|
||||
<div className="hidden md:block">拖拽图片 - 切换图片</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user