ui: 滑卡互动增强 — like 徽章动画、分类标签可读性、进度数字区分
- RestaurantCard: like 徽章实时弹跳动画,首次出现滑入效果 - RestaurantCard: 分类标签改为白底黑字,图片上始终清晰 - SwipeDeck: 进度数字加背景色块包裹,与用户名视觉分离
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useState, useEffect } from "react";
|
||||
import { useCallback, useState, useEffect, useRef } from "react";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import { Star, MapPin, Clock, ExternalLink, Flame, Bookmark, ChevronLeft, ChevronRight } from "lucide-react";
|
||||
import { Restaurant } from "@/types";
|
||||
import { getUserId, isRegistered } from "@/lib/userId";
|
||||
@@ -118,6 +119,17 @@ function ImageGallery({ images, name }: { images: string[]; name: string }) {
|
||||
|
||||
export default function RestaurantCard({ restaurant, likeCount = 0 }: RestaurantCardProps) {
|
||||
const [favorited, setFavorited] = useState(false);
|
||||
const [likeBounce, setLikeBounce] = useState(false);
|
||||
const prevLikeRef = useRef(likeCount);
|
||||
|
||||
useEffect(() => {
|
||||
if (likeCount > prevLikeRef.current) {
|
||||
setLikeBounce(true);
|
||||
const t = setTimeout(() => setLikeBounce(false), 600);
|
||||
return () => clearTimeout(t);
|
||||
}
|
||||
prevLikeRef.current = likeCount;
|
||||
}, [likeCount]);
|
||||
|
||||
const images = restaurant.images?.filter(Boolean);
|
||||
const hasImage = images && images.length > 0;
|
||||
@@ -156,16 +168,32 @@ export default function RestaurantCard({ restaurant, likeCount = 0 }: Restaurant
|
||||
|
||||
<div className="absolute bottom-3 left-4 flex items-center gap-1.5">
|
||||
{restaurant.category && (
|
||||
<span className="rounded-full bg-elevated/90 px-2.5 py-0.5 text-xs font-semibold text-gray-200 shadow-sm backdrop-blur-sm">
|
||||
<span className="rounded-full bg-white/80 px-2.5 py-0.5 text-xs font-semibold text-gray-800 shadow-sm backdrop-blur-sm">
|
||||
{restaurant.category}
|
||||
</span>
|
||||
)}
|
||||
{likeCount > 0 && (
|
||||
<span className="flex items-center gap-0.5 rounded-full bg-rose-500/90 px-2 py-0.5 text-xs font-semibold text-white shadow-sm backdrop-blur-sm">
|
||||
<Flame size={11} />
|
||||
{likeCount} 人想去
|
||||
</span>
|
||||
)}
|
||||
<AnimatePresence>
|
||||
{likeCount > 0 && (
|
||||
<motion.span
|
||||
key="like-badge"
|
||||
className="flex items-center gap-0.5 rounded-full bg-rose-500/90 px-2 py-0.5 text-xs font-semibold text-white shadow-sm backdrop-blur-sm"
|
||||
initial={{ opacity: 0, scale: 0.5, x: -8 }}
|
||||
animate={{
|
||||
opacity: 1,
|
||||
scale: likeBounce ? [1, 1.3, 1] : 1,
|
||||
x: 0,
|
||||
}}
|
||||
exit={{ opacity: 0, scale: 0.5 }}
|
||||
transition={likeBounce
|
||||
? { scale: { duration: 0.4, ease: "easeInOut" }, default: { type: "spring", stiffness: 400, damping: 20 } }
|
||||
: { type: "spring", stiffness: 400, damping: 20 }
|
||||
}
|
||||
>
|
||||
<Flame size={11} className={likeBounce ? "animate-pulse" : ""} />
|
||||
{likeCount} 人想去
|
||||
</motion.span>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -34,11 +34,14 @@ function UserProgressBar({
|
||||
return (
|
||||
<div className="flex items-center gap-x-3">
|
||||
<div className="flex flex-1 flex-wrap items-center gap-x-3 gap-y-1">
|
||||
<span className="flex items-center gap-1 text-[11px] tabular-nums text-accent">
|
||||
<span className="flex items-center gap-1.5 text-[11px] text-accent">
|
||||
<span className={`inline-flex h-4 w-4 items-center justify-center rounded-full ${myAvatarBg} text-[10px] leading-none`}>
|
||||
{myAvatar}
|
||||
</span>
|
||||
你 {localIndex}/{total}
|
||||
你
|
||||
<span className="rounded bg-accent/10 px-1 py-px tabular-nums font-medium">
|
||||
{localIndex}/{total}
|
||||
</span>
|
||||
</span>
|
||||
{others.map(([id, count]) => {
|
||||
const finished = count >= total;
|
||||
@@ -49,13 +52,15 @@ function UserProgressBar({
|
||||
return (
|
||||
<span
|
||||
key={id}
|
||||
className={`flex items-center gap-1 text-[11px] tabular-nums ${finished ? "text-emerald-400" : "text-muted"}`}
|
||||
className={`flex items-center gap-1.5 text-[11px] ${finished ? "text-emerald-400" : "text-muted"}`}
|
||||
>
|
||||
<span className={`inline-flex h-4 w-4 items-center justify-center rounded-full ${bg} text-[10px] leading-none`}>
|
||||
{emoji}
|
||||
</span>
|
||||
{label && <span className="max-w-12 truncate">{label}</span>}
|
||||
{count}/{total}
|
||||
<span className={`rounded px-1 py-px tabular-nums font-medium ${finished ? "bg-emerald-500/10" : "bg-elevated"}`}>
|
||||
{count}/{total}
|
||||
</span>
|
||||
{finished && <Check size={10} className="text-emerald-400" />}
|
||||
</span>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user