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 (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}