From b2b18327cc7989471f35b496ddaa16fa1dc60438 Mon Sep 17 00:00:00 2001 From: kurihada Date: Thu, 26 Feb 2026 16:11:01 +0800 Subject: [PATCH] =?UTF-8?q?ui:=20=E9=AA=A8=E6=9E=B6=E5=B1=8F=E6=9B=BF?= =?UTF-8?q?=E4=BB=A3=E5=85=A8=E9=83=A8=E9=A1=B5=E9=9D=A2=E7=BA=A7=E5=8A=A0?= =?UTF-8?q?=E8=BD=BD=20spinner?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 Skeleton 组件库:Skeleton、SkeletonCircle 基础元素 + 5 个业务骨架 (SwipeDeck、ProfileCard、RecordItem、BlindboxRoom、BlindboxList、RoomCard) - 替换 room、profile、blindbox 列表、blindbox 房间、invite 5 个页面的加载态 - 替换 profile 历史记录 / 收藏列表的内联加载 spinner - 更新 project-conventions.mdc:新增 Loading States 规范, 要求页面级和列表级加载必须使用骨架屏 --- .cursor/rules/project-conventions.mdc | 8 ++ src/app/blindbox/[code]/page.tsx | 7 +- src/app/blindbox/page.tsx | 12 +-- src/app/invite/[id]/page.tsx | 20 +++- src/app/profile/page.tsx | 33 ++++-- src/app/room/[id]/page.tsx | 8 +- src/components/Skeleton.tsx | 141 ++++++++++++++++++++++++++ 7 files changed, 200 insertions(+), 29 deletions(-) create mode 100644 src/components/Skeleton.tsx diff --git a/.cursor/rules/project-conventions.mdc b/.cursor/rules/project-conventions.mdc index 695b350..2eaf669 100644 --- a/.cursor/rules/project-conventions.mdc +++ b/.cursor/rules/project-conventions.mdc @@ -35,6 +35,14 @@ alwaysApply: true - `overflow-y-auto scrollbar-none` for scrollable pages - Extract reusable UI patterns into shared components (modals, cards, empty states) +## Loading States + +- **Page-level and list-level loading**: always use skeleton screens (`src/components/Skeleton.tsx`), never bare spinners +- Skeleton shape should mimic the actual content layout (cards, list items, avatars, text lines) +- Compose page skeletons from reusable skeleton primitives: `Skeleton`, `SkeletonCircle`, `RecordItemSkeleton`, `RoomCardSkeleton`, etc. +- **Button-level loading** (submit, save, join): keep using inline `Loader2` spinner — skeleton screens don't apply to buttons +- When adding a new page or async data section, always include a skeleton loading state + ## API Routes - Located in `src/app/api/` diff --git a/src/app/blindbox/[code]/page.tsx b/src/app/blindbox/[code]/page.tsx index cfc9ff0..25e5f93 100644 --- a/src/app/blindbox/[code]/page.tsx +++ b/src/app/blindbox/[code]/page.tsx @@ -20,6 +20,7 @@ import { import confetti from "canvas-confetti"; import { getCachedProfile, isRegistered } from "@/lib/userId"; import ShareCardModal from "@/components/ShareCardModal"; +import { BlindboxRoomSkeleton } from "@/components/Skeleton"; import type { UserProfile } from "@/types"; interface RoomInfo { @@ -285,11 +286,7 @@ export default function BlindboxRoomPage() { }; if (pageLoading) { - return ( -
- -
- ); + return ; } if (!room) return null; diff --git a/src/app/blindbox/page.tsx b/src/app/blindbox/page.tsx index 0eb27b6..86f5c52 100644 --- a/src/app/blindbox/page.tsx +++ b/src/app/blindbox/page.tsx @@ -15,6 +15,7 @@ import { } from "lucide-react"; import { getCachedProfile, isRegistered } from "@/lib/userId"; import AuthModal from "@/components/AuthModal"; +import { BlindboxListSkeleton } from "@/components/Skeleton"; import type { UserProfile } from "@/types"; interface RoomSummary { @@ -216,16 +217,7 @@ export default function BlindboxLobbyPage() {

) : loading ? ( - /* ============ Loading ============ */ - - -

加载中...

-
+ ) : rooms.length === 0 ? ( /* ============ Layer 2: Logged in, no rooms — Create first ============ */ -
+
+ + + +
+
+ + + +
+
+
+ + + +
+
); } diff --git a/src/app/profile/page.tsx b/src/app/profile/page.tsx index ba1915d..ad22db1 100644 --- a/src/app/profile/page.tsx +++ b/src/app/profile/page.tsx @@ -25,6 +25,7 @@ import { } from "lucide-react"; import EmptyState from "@/components/EmptyState"; 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 type { UserProfile, UserPreferences, DecisionRecord, FavoriteRecord, Restaurant } from "@/types"; @@ -256,8 +257,26 @@ export default function ProfilePage() { if (loading) { return ( -
- +
+ +
+ +
+
+ + +
+
+
+
+ + +
+
+
); } @@ -534,8 +553,9 @@ export default function ProfilePage() { className="overflow-hidden" > {historyLoading ? ( -
- +
+ +
) : history.length === 0 ? ( {favLoading ? ( -
- +
+ +
) : favorites.length === 0 ? ( -
-

正在加载数据...

-
- ); + return ; } return ( diff --git a/src/components/Skeleton.tsx b/src/components/Skeleton.tsx new file mode 100644 index 0000000..34e5015 --- /dev/null +++ b/src/components/Skeleton.tsx @@ -0,0 +1,141 @@ +"use client"; + +interface SkeletonProps { + className?: string; +} + +export function Skeleton({ className = "" }: SkeletonProps) { + return ( +
+ ); +} + +export function SkeletonCircle({ className = "" }: SkeletonProps) { + return ( +
+ ); +} + +export function RoomCardSkeleton() { + return ( +
+ +
+ + +
+ +
+ ); +} + +export function ProfileCardSkeleton() { + return ( +
+
+ +
+ + +
+
+
+ ); +} + +export function RecordItemSkeleton() { + return ( +
+ +
+ + +
+
+ ); +} + +export function SwipeDeckSkeleton() { + return ( +
+ +
+
+
+ +
+ +
+ + + +
+ + +
+
+
+
+
+ + +
+
+ ); +} + +export function BlindboxRoomSkeleton() { + return ( +
+
+ +
+ + +
+
+ + +
+ +
+ + + +
+ + +
+
+ ); +} + +export function BlindboxListSkeleton() { + return ( +
+
+ + +
+
+ + +
+
+ + + +
+
+ ); +}