From 079feddf0efc36e955455da07d063f78475d42b6 Mon Sep 17 00:00:00 2001 From: kurihada Date: Wed, 25 Feb 2026 11:51:42 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=95=86=E5=AE=B6=E5=8D=A1=E7=89=87?= =?UTF-8?q?=E6=94=AF=E6=8C=81=E5=A4=9A=E5=9B=BE=E5=B1=95=E7=A4=BA=EF=BC=8C?= =?UTF-8?q?=E7=82=B9=E5=87=BB=E5=B7=A6=E5=8F=B3=E5=88=87=E6=8D=A2=E5=B8=A6?= =?UTF-8?q?=20crossfade=20=E8=BF=87=E6=B8=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Restaurant.image 改为 images: string[],API 层从高德取最多 5 张图 - RestaurantCard 新增 ImageGallery:点击图片左右区域切换、顶部圆点指示器、 左右箭头提示、首次查看时文字引导气泡(2.5s 自动消失) - 图片切换使用 crossfade 动画(旧图渐隐 280ms),过渡平滑 - MatchResult / Profile 页面兼容新旧数据格式,无图时条件渲染 --- src/app/api/room/create/route.ts | 14 ++-- src/app/globals.css | 5 ++ src/app/profile/page.tsx | 15 +++- src/components/MatchResult.tsx | 28 ++++--- src/components/RestaurantCard.tsx | 122 +++++++++++++++++++++++++++--- src/types/index.ts | 2 +- 6 files changed, 153 insertions(+), 33 deletions(-) diff --git a/src/app/api/room/create/route.ts b/src/app/api/room/create/route.ts index 94bbcdc..d4c0921 100644 --- a/src/app/api/room/create/route.ts +++ b/src/app/api/room/create/route.ts @@ -42,10 +42,14 @@ function mapPoiToRestaurant(poi: AmapPoiV5, defaultImage: string): Restaurant { const price = costStr && costStr !== "[]" && costStr !== "0" ? `¥${costStr}` : "未知"; - const image = - poi.photos && poi.photos.length > 0 && poi.photos[0].url - ? poi.photos[0].url - : defaultImage; + const images = + poi.photos && poi.photos.length > 0 + ? poi.photos + .map((p) => p.url) + .filter(Boolean) + .slice(0, 5) + : []; + if (images.length === 0) images.push(defaultImage); const openTime = cleanField(poi.business?.opentime_week) || @@ -57,7 +61,7 @@ function mapPoiToRestaurant(poi: AmapPoiV5, defaultImage: string): Restaurant { rating, price, distance: poi.distance ? `${poi.distance}m` : "", - image, + images, category: extractCategory(poi.type), address: cleanField(poi.address), openTime, diff --git a/src/app/globals.css b/src/app/globals.css index f76e68f..22f9ba3 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -34,3 +34,8 @@ body { .scrollbar-none::-webkit-scrollbar { display: none; } + +@keyframes img-fade-out { + from { opacity: 1; } + to { opacity: 0; } +} diff --git a/src/app/profile/page.tsx b/src/app/profile/page.tsx index 12f278e..041af5b 100644 --- a/src/app/profile/page.tsx +++ b/src/app/profile/page.tsx @@ -24,6 +24,13 @@ import { getUserId, getCachedProfile, setCachedProfile, setCachedPreferences, lo import { getAvatarBg, AVATARS } from "@/lib/avatars"; import type { UserProfile, UserPreferences, DecisionRecord, FavoriteRecord, Restaurant } from "@/types"; +function firstImage(r: Restaurant): string { + if (r.images?.length > 0) return r.images[0]; + // backward compat: old DB records may have `image` instead of `images` + const legacy = (r as Record).image; + return typeof legacy === "string" ? legacy : ""; +} + export default function ProfilePage() { const router = useRouter(); const [userId, setUserId] = useState(""); @@ -534,9 +541,9 @@ export default function ProfilePage() { rel="noopener noreferrer" className="flex gap-3 rounded-xl bg-zinc-50 p-2.5 transition-colors active:bg-zinc-100" > - {d.restaurantData.image && ( + {firstImage(d.restaurantData) && ( {d.restaurantName} - {r.image && ( + {firstImage(r) && ( {r.name} - {restaurant.name} + {restaurant.images?.[0] && ( + {restaurant.name} + )}

{restaurant.name} @@ -348,12 +350,14 @@ export default function MatchResult({ animate={{ y: 0, opacity: 1 }} transition={{ type: "spring", stiffness: 180, damping: 18, delay: 0.5 }} > - {restaurant.name} + {restaurant.images?.[0] && ( + {restaurant.name} + )}

diff --git a/src/components/RestaurantCard.tsx b/src/components/RestaurantCard.tsx index e65558c..c22cf39 100644 --- a/src/components/RestaurantCard.tsx +++ b/src/components/RestaurantCard.tsx @@ -1,7 +1,7 @@ "use client"; -import { useCallback, useState } from "react"; -import { Star, MapPin, Clock, ExternalLink, Flame, Bookmark } from "lucide-react"; +import { useCallback, useState, useEffect } from "react"; +import { Star, MapPin, Clock, ExternalLink, Flame, Bookmark, ChevronLeft, ChevronRight } from "lucide-react"; import { Restaurant } from "@/types"; import { getUserId, isRegistered } from "@/lib/userId"; @@ -14,9 +14,115 @@ function stopAll(e: React.SyntheticEvent) { e.stopPropagation(); } +function ImageGallery({ images, name }: { images: string[]; name: string }) { + const [idx, setIdx] = useState(0); + const [fadingOut, setFadingOut] = useState(null); + const [showHint, setShowHint] = useState(true); + const count = images.length; + const hasMultiple = count > 1; + + useEffect(() => { + if (!hasMultiple || !showHint) return; + const t = setTimeout(() => setShowHint(false), 2500); + return () => clearTimeout(t); + }, [hasMultiple, showHint]); + + const goTo = useCallback( + (newIdx: number) => { + if (newIdx === idx) return; + setFadingOut(idx); + setIdx(newIdx); + }, + [idx], + ); + + const handleTap = useCallback( + (e: React.MouseEvent) => { + if (!hasMultiple) return; + e.stopPropagation(); + e.preventDefault(); + setShowHint(false); + + const rect = e.currentTarget.getBoundingClientRect(); + const tapX = e.clientX - rect.left; + const isRight = tapX > rect.width / 2; + + const newIdx = isRight + ? Math.min(idx + 1, count - 1) + : Math.max(idx - 1, 0); + goTo(newIdx); + }, + [hasMultiple, count, idx, goTo], + ); + + return ( +
+ {name} + + {fadingOut !== null && ( + setFadingOut(null)} + draggable={false} + referrerPolicy="no-referrer" + /> + )} + + {hasMultiple && ( + <> +
+ {images.map((_, i) => ( + + ))} +
+ + {idx > 0 && ( +
+ +
+ )} + {idx < count - 1 && ( +
+ +
+ )} + + {showHint && idx === 0 && ( +
+ + 点击图片左右切换 · 共 {count} 张 + +
+ )} + + )} +
+ ); +} + export default function RestaurantCard({ restaurant, likeCount = 0 }: RestaurantCardProps) { const [favorited, setFavorited] = useState(false); + const images = restaurant.images?.filter(Boolean); + const hasImage = images && images.length > 0; + const handleFavorite = useCallback(async (e: React.MouseEvent | React.TouchEvent) => { e.stopPropagation(); e.preventDefault(); @@ -45,15 +151,9 @@ export default function RestaurantCard({ restaurant, likeCount = 0 }: Restaurant return (
-
- {restaurant.name} -
+
+ {hasImage && } +
{restaurant.category && ( diff --git a/src/types/index.ts b/src/types/index.ts index 7ff5fe0..76a6b20 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -6,7 +6,7 @@ export interface Restaurant { rating: number; price: string; distance: string; - image: string; + images: string[]; category: string; address: string; openTime: string;