ui: 滑卡互动增强 — like 徽章动画、分类标签可读性、进度数字区分

- RestaurantCard: like 徽章实时弹跳动画,首次出现滑入效果
- RestaurantCard: 分类标签改为白底黑字,图片上始终清晰
- SwipeDeck: 进度数字加背景色块包裹,与用户名视觉分离
This commit is contained in:
2026-02-26 16:32:00 +08:00
parent 9759db54ca
commit 37eb7f07d7
2 changed files with 45 additions and 12 deletions
+36 -8
View File
@@ -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>
+9 -4
View File
@@ -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>
);