From 2a3cef890cd7888d40265fe3ff66ff7b67c92430 Mon Sep 17 00:00:00 2001 From: kurihada Date: Thu, 26 Feb 2026 19:59:35 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E6=8B=86=E5=88=86=20MatchResult?= =?UTF-8?q?=E3=80=81ProfilePage=E3=80=81BlindboxRoomPage=20=E5=A4=A7?= =?UTF-8?q?=E7=BB=84=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - MatchResult: 提取 NoMatchResult、RunnerUpCard(635 → 513 行) - ProfilePage: 提取 ProfileHistoryCard、ProfileFavoritesCard(692 → 526 行) - BlindboxRoomPage: 提取 BlindboxMyIdeas、BlindboxDrawnHistory(855 → 668 行) --- src/app/blindbox/[code]/page.tsx | 188 +--------------------- src/app/profile/page.tsx | 205 +++--------------------- src/components/BlindboxDrawnHistory.tsx | 70 ++++++++ src/components/BlindboxMyIdeas.tsx | 121 ++++++++++++++ src/components/MatchResult.tsx | 125 +-------------- src/components/NoMatchResult.tsx | 78 +++++++++ src/components/ProfileFavoritesCard.tsx | 128 +++++++++++++++ src/components/ProfileHistoryCard.tsx | 113 +++++++++++++ src/components/RunnerUpCard.tsx | 52 ++++++ 9 files changed, 589 insertions(+), 491 deletions(-) create mode 100644 src/components/BlindboxDrawnHistory.tsx create mode 100644 src/components/BlindboxMyIdeas.tsx create mode 100644 src/components/NoMatchResult.tsx create mode 100644 src/components/ProfileFavoritesCard.tsx create mode 100644 src/components/ProfileHistoryCard.tsx create mode 100644 src/components/RunnerUpCard.tsx diff --git a/src/app/blindbox/[code]/page.tsx b/src/app/blindbox/[code]/page.tsx index ea3220e..6ca60b7 100644 --- a/src/app/blindbox/[code]/page.tsx +++ b/src/app/blindbox/[code]/page.tsx @@ -9,21 +9,19 @@ import { Loader2, Package, Flame, - Trophy, Users, Share2, LogIn, Copy, Trash2, LogOut, - Pencil, - Check, - X, } from "lucide-react"; import confetti from "canvas-confetti"; import { getCachedProfile, isRegistered } from "@/lib/userId"; import ShareCardModal from "@/components/ShareCardModal"; import Button from "@/components/Button"; +import BlindboxMyIdeas, { type MyIdea } from "@/components/BlindboxMyIdeas"; +import BlindboxDrawnHistory, { type DrawnIdea } from "@/components/BlindboxDrawnHistory"; import { useToast } from "@/hooks/useToast"; import { useShare } from "@/hooks/useShare"; import { BlindboxRoomSkeleton } from "@/components/Skeleton"; @@ -38,132 +36,8 @@ interface RoomInfo { members: { id: string; username: string; avatar: string }[]; } -interface MyIdea { - id: string; - content: string; - createdAt: string; -} - -interface DrawnIdea { - id: string; - content: string; - createdAt: string; - user?: { id: string; username: string; avatar: string }; - drawnBy?: { id: string; username: string; avatar: string } | null; -} - type Phase = "pool" | "shaking" | "reveal"; -function MyIdeaItem({ - idea, - onEdit, - onDelete, -}: { - idea: MyIdea; - onEdit: (id: string, content: string) => Promise; - onDelete: (id: string) => Promise; -}) { - const [editing, setEditing] = useState(false); - const [draft, setDraft] = useState(idea.content); - const [saving, setSaving] = useState(false); - - const handleSave = async () => { - if (!draft.trim() || saving) return; - setSaving(true); - await onEdit(idea.id, draft); - setSaving(false); - setEditing(false); - }; - - return ( - - {editing ? ( - <> - setDraft(e.target.value.slice(0, 200))} - onKeyDown={(e) => { if (e.key === "Enter") handleSave(); if (e.key === "Escape") { setEditing(false); setDraft(idea.content); } }} - maxLength={200} - autoFocus - className="h-8 min-w-0 flex-1 rounded-lg bg-elevated px-2.5 text-sm text-foreground outline-none ring-1 ring-border focus:ring-2 focus:ring-purple-600/50" - /> - - - - ) : ( - <> - 💡 -

{idea.content}

- - - - )} -
- ); -} - -function MyIdeasSection({ - ideas, - onEdit, - onDelete, -}: { - ideas: MyIdea[]; - onEdit: (id: string, content: string) => Promise; - onDelete: (id: string) => Promise; -}) { - return ( - -
- -

- 我投入的想法({ideas.length}) -

-
-
-
- - {ideas.map((idea) => ( - - ))} - -
- - ); -} - export default function BlindboxRoomPage() { const { code } = useParams<{ code: string }>(); const router = useRouter(); @@ -729,68 +603,16 @@ export default function BlindboxRoomPage() { )} - {/* My ideas in pool */} {myIdeas.length > 0 && phase === "pool" && ( - )} - {/* History */} - {drawnHistory.length > 0 && phase !== "shaking" && ( - -
- -

- 履约记录 -

-
-
-
- {drawnHistory.map((item, i) => ( - - 🏆 -
-

- {item.content} -

-
- {item.user && ( - {item.user.avatar} {item.user.username} 投入 - )} - {item.drawnBy && ( - <> - · - {item.drawnBy.avatar} {item.drawnBy.username} 抽中 - - )} - · - - {new Date(item.createdAt).toLocaleDateString("zh-CN", { - month: "short", - day: "numeric", - weekday: "short", - })} - -
-
-
- ))} -
- + {phase !== "shaking" && ( + )} )} diff --git a/src/app/profile/page.tsx b/src/app/profile/page.tsx index 1ce8109..ac5d760 100644 --- a/src/app/profile/page.tsx +++ b/src/app/profile/page.tsx @@ -6,12 +6,7 @@ import { motion, AnimatePresence } from "framer-motion"; import { ArrowLeft, Mail, - Clock, - Star, - MapPin, - Trash2, Loader2, - ChevronDown, LogOut, Lock, Edit3, @@ -20,25 +15,16 @@ import { Eye, EyeOff, Zap, - ClipboardList, - Heart, } from "lucide-react"; -import EmptyState from "@/components/EmptyState"; import Card from "@/components/Card"; import Input from "@/components/Input"; +import ProfileHistoryCard from "@/components/ProfileHistoryCard"; +import ProfileFavoritesCard from "@/components/ProfileFavoritesCard"; import { useToast } from "@/hooks/useToast"; -import RestaurantImage from "@/components/RestaurantImage"; import { ProfileCardSkeleton, RecordItemSkeleton } from "@/components/Skeleton"; import { getUserId, getCachedProfile, setCachedProfile, setCachedPreferences, logout } from "@/lib/userId"; import { getAvatarBg, AVATARS } from "@/lib/avatars"; -import { buildNavUrl } from "@/lib/navigation"; -import type { UserProfile, UserPreferences, DecisionRecord, FavoriteRecord, Restaurant } from "@/types"; - -function firstImage(r: Restaurant): string { - if (r.images?.length > 0) return r.images[0]; - const legacy = (r as unknown as Record).image; - return typeof legacy === "string" ? legacy : ""; -} +import type { UserProfile, UserPreferences, DecisionRecord, FavoriteRecord } from "@/types"; export default function ProfilePage() { const router = useRouter(); @@ -499,175 +485,24 @@ export default function ProfilePage() { )} - {/* Decision History */} - - + setShowHistory((v) => !v)} + onEmpty={() => router.push("/blindbox")} + delay={0.15} + /> - - {showHistory && ( - - {historyLoading ? ( -
- - -
- ) : history.length === 0 ? ( - router.push("/blindbox")} - color="purple" - /> - ) : ( - - )} -
- )} -
-
- - {/* Favorites */} - - - - - {showFavorites && ( - - {favLoading ? ( -
- - -
- ) : favorites.length === 0 ? ( - router.push("/blindbox")} - color="amber" - /> - ) : ( -
- {favorites.map((f) => { - const r = f.restaurantData; - return ( -
- {firstImage(r) && ( - - )} -
-

{r.name}

-
- - - {r.rating} - - {r.price} - {r.distance && ( - - - {r.distance} - - )} -
-
- -
- ); - })} -
- )} -
- )} -
-
+ setShowFavorites((v) => !v)} + onRemove={handleRemoveFavorite} + onEmpty={() => router.push("/blindbox")} + delay={0.2} + /> {/* Logout */} +
+ +

+ 履约记录 +

+
+
+
+ {items.map((item, i) => ( + + 🏆 +
+

+ {item.content} +

+
+ {item.user && ( + {item.user.avatar} {item.user.username} 投入 + )} + {item.drawnBy && ( + <> + · + {item.drawnBy.avatar} {item.drawnBy.username} 抽中 + + )} + · + + {new Date(item.createdAt).toLocaleDateString("zh-CN", { + month: "short", + day: "numeric", + weekday: "short", + })} + +
+
+
+ ))} +
+ + ); +} diff --git a/src/components/BlindboxMyIdeas.tsx b/src/components/BlindboxMyIdeas.tsx new file mode 100644 index 0000000..d8e3019 --- /dev/null +++ b/src/components/BlindboxMyIdeas.tsx @@ -0,0 +1,121 @@ +"use client"; + +import { useState } from "react"; +import { motion, AnimatePresence } from "framer-motion"; +import { Package, Loader2, Pencil, Trash2, Check, X } from "lucide-react"; + +export interface MyIdea { + id: string; + content: string; + createdAt: string; +} + +function MyIdeaItem({ + idea, + onEdit, + onDelete, +}: { + idea: MyIdea; + onEdit: (id: string, content: string) => Promise; + onDelete: (id: string) => Promise; +}) { + const [editing, setEditing] = useState(false); + const [draft, setDraft] = useState(idea.content); + const [saving, setSaving] = useState(false); + + const handleSave = async () => { + if (!draft.trim() || saving) return; + setSaving(true); + await onEdit(idea.id, draft); + setSaving(false); + setEditing(false); + }; + + return ( + + {editing ? ( + <> + setDraft(e.target.value.slice(0, 200))} + onKeyDown={(e) => { if (e.key === "Enter") handleSave(); if (e.key === "Escape") { setEditing(false); setDraft(idea.content); } }} + maxLength={200} + autoFocus + className="h-8 min-w-0 flex-1 rounded-lg bg-elevated px-2.5 text-sm text-foreground outline-none ring-1 ring-border focus:ring-2 focus:ring-purple-600/50" + /> + + + + ) : ( + <> + 💡 +

{idea.content}

+ + + + )} +
+ ); +} + +export default function BlindboxMyIdeas({ + ideas, + onEdit, + onDelete, +}: { + ideas: MyIdea[]; + onEdit: (id: string, content: string) => Promise; + onDelete: (id: string) => Promise; +}) { + return ( + +
+ +

+ 我投入的想法({ideas.length}) +

+
+
+
+ + {ideas.map((idea) => ( + + ))} + +
+ + ); +} diff --git a/src/components/MatchResult.tsx b/src/components/MatchResult.tsx index 90ec7d2..7bf7bd0 100644 --- a/src/components/MatchResult.tsx +++ b/src/components/MatchResult.tsx @@ -12,8 +12,6 @@ import { Clock, Trophy, RotateCcw, - SearchX, - Home, ChevronDown, Swords, RefreshCw, @@ -35,6 +33,8 @@ import ShareCardModal from "@/components/ShareCardModal"; import RestaurantImage from "@/components/RestaurantImage"; import AuthModal from "@/components/AuthModal"; import Button from "@/components/Button"; +import NoMatchResult from "@/components/NoMatchResult"; +import RunnerUpCard from "@/components/RunnerUpCard"; import { buildNavUrl } from "@/lib/navigation"; import { useToast } from "@/hooks/useToast"; @@ -53,127 +53,6 @@ interface MatchResultProps { scene?: SceneType; } -function NoMatchResult({ - onReset, - resetting, -}: { - onReset: () => Promise; - resetting: boolean; -}) { - const router = useRouter(); - - return ( - - - - - - - 都不太满意 - - - - 这一轮没有店被选中,换个范围或类型再试试? - - - - - - - - - ); -} - -function RunnerUpCard({ - restaurant, - likes, - userCount, -}: { - restaurant: Restaurant; - likes: number; - userCount: number; -}) { - return ( - - {restaurant.images?.[0] && ( - - )} -
-

- {restaurant.name} -

-
- - - {restaurant.rating} - - {restaurant.price} - {restaurant.distance && ( - - - {restaurant.distance} - - )} -
-

- {likes}/{userCount} 人想去 -

-
-
- ); -} - export default function MatchResult({ restaurant, matchType, diff --git a/src/components/NoMatchResult.tsx b/src/components/NoMatchResult.tsx new file mode 100644 index 0000000..6cb0060 --- /dev/null +++ b/src/components/NoMatchResult.tsx @@ -0,0 +1,78 @@ +"use client"; + +import { motion } from "framer-motion"; +import { useRouter } from "next/navigation"; +import { SearchX, RotateCcw, Home } from "lucide-react"; +import Button from "@/components/Button"; + +interface NoMatchResultProps { + onReset: () => Promise; + resetting: boolean; +} + +export default function NoMatchResult({ onReset, resetting }: NoMatchResultProps) { + const router = useRouter(); + + return ( + + + + + + + 都不太满意 + + + + 这一轮没有店被选中,换个范围或类型再试试? + + + + + + + + + ); +} diff --git a/src/components/ProfileFavoritesCard.tsx b/src/components/ProfileFavoritesCard.tsx new file mode 100644 index 0000000..72fae12 --- /dev/null +++ b/src/components/ProfileFavoritesCard.tsx @@ -0,0 +1,128 @@ +"use client"; + +import { motion, AnimatePresence } from "framer-motion"; +import { Star, MapPin, ChevronDown, Trash2, Heart } from "lucide-react"; +import Card from "@/components/Card"; +import EmptyState from "@/components/EmptyState"; +import RestaurantImage from "@/components/RestaurantImage"; +import { RecordItemSkeleton } from "@/components/Skeleton"; +import type { FavoriteRecord, Restaurant } from "@/types"; + +function firstImage(r: Restaurant): string { + if (r.images?.length > 0) return r.images[0]; + const legacy = (r as unknown as Record).image; + return typeof legacy === "string" ? legacy : ""; +} + +interface ProfileFavoritesCardProps { + favorites: FavoriteRecord[]; + loading: boolean; + open: boolean; + onToggle: () => void; + onRemove: (id: string) => Promise; + onEmpty: () => void; + delay?: number; +} + +export default function ProfileFavoritesCard({ + favorites, + loading, + open, + onToggle, + onRemove, + onEmpty, + delay, +}: ProfileFavoritesCardProps) { + return ( + + + + + {open && ( + + {loading ? ( +
+ + +
+ ) : favorites.length === 0 ? ( + + ) : ( +
+ {favorites.map((f) => { + const r = f.restaurantData; + return ( +
+ {firstImage(r) && ( + + )} +
+

{r.name}

+
+ + + {r.rating} + + {r.price} + {r.distance && ( + + + {r.distance} + + )} +
+
+ +
+ ); + })} +
+ )} +
+ )} +
+
+ ); +} diff --git a/src/components/ProfileHistoryCard.tsx b/src/components/ProfileHistoryCard.tsx new file mode 100644 index 0000000..faa1e5a --- /dev/null +++ b/src/components/ProfileHistoryCard.tsx @@ -0,0 +1,113 @@ +"use client"; + +import { motion, AnimatePresence } from "framer-motion"; +import { Clock, ChevronDown, ClipboardList } from "lucide-react"; +import Card from "@/components/Card"; +import EmptyState from "@/components/EmptyState"; +import RestaurantImage from "@/components/RestaurantImage"; +import { RecordItemSkeleton } from "@/components/Skeleton"; +import { buildNavUrl } from "@/lib/navigation"; +import type { DecisionRecord, Restaurant } from "@/types"; + +function firstImage(r: Restaurant): string { + if (r.images?.length > 0) return r.images[0]; + const legacy = (r as unknown as Record).image; + return typeof legacy === "string" ? legacy : ""; +} + +interface ProfileHistoryCardProps { + history: DecisionRecord[]; + loading: boolean; + open: boolean; + onToggle: () => void; + onEmpty: () => void; + delay?: number; +} + +export default function ProfileHistoryCard({ + history, + loading, + open, + onToggle, + onEmpty, + delay, +}: ProfileHistoryCardProps) { + return ( + + + + + {open && ( + + {loading ? ( +
+ + +
+ ) : history.length === 0 ? ( + + ) : ( + + )} +
+ )} +
+
+ ); +} diff --git a/src/components/RunnerUpCard.tsx b/src/components/RunnerUpCard.tsx new file mode 100644 index 0000000..304939a --- /dev/null +++ b/src/components/RunnerUpCard.tsx @@ -0,0 +1,52 @@ +"use client"; + +import { Star, MapPin } from "lucide-react"; +import type { Restaurant } from "@/types"; +import RestaurantImage from "@/components/RestaurantImage"; +import { buildNavUrl } from "@/lib/navigation"; + +interface RunnerUpCardProps { + restaurant: Restaurant; + likes: number; + userCount: number; +} + +export default function RunnerUpCard({ restaurant, likes, userCount }: RunnerUpCardProps) { + return ( + + {restaurant.images?.[0] && ( + + )} +
+

+ {restaurant.name} +

+
+ + + {restaurant.rating} + + {restaurant.price} + {restaurant.distance && ( + + + {restaurant.distance} + + )} +
+

+ {likes}/{userCount} 人想去 +

+
+
+ ); +}