diff --git a/src/app/blindbox/[code]/page.tsx b/src/app/blindbox/[code]/page.tsx
index 97d737f..57e2e1b 100644
--- a/src/app/blindbox/[code]/page.tsx
+++ b/src/app/blindbox/[code]/page.tsx
@@ -23,6 +23,7 @@ import {
import confetti from "canvas-confetti";
import { getCachedProfile, isRegistered } from "@/lib/userId";
import ShareCardModal from "@/components/ShareCardModal";
+import Button from "@/components/Button";
import { useToast } from "@/hooks/useToast";
import { BlindboxRoomSkeleton } from "@/components/Skeleton";
import type { UserProfile } from "@/types";
@@ -536,14 +537,15 @@ export default function BlindboxRoomPage() {
>
你还不是这个房间的成员
- }
>
- {joiningRoom ? : }
加入房间
-
+
) : (
<>
@@ -717,21 +719,21 @@ export default function BlindboxRoomPage() {
- setShowShareCard(true)}
- className="flex h-10 items-center gap-2 rounded-full bg-purple-600 px-5 text-xs font-bold text-white shadow-lg shadow-purple-900/30 transition-colors hover:bg-purple-500"
- whileTap={{ scale: 0.96 }}
+ variant="purple"
+ shape="pill"
+ icon={}
>
-
分享契约
-
-
+
+
)}
diff --git a/src/app/blindbox/page.tsx b/src/app/blindbox/page.tsx
index 86f5c52..8353535 100644
--- a/src/app/blindbox/page.tsx
+++ b/src/app/blindbox/page.tsx
@@ -10,11 +10,11 @@ import {
LogIn,
Users,
Sparkles,
- Loader2,
ChevronRight,
} from "lucide-react";
import { getCachedProfile, isRegistered } from "@/lib/userId";
import AuthModal from "@/components/AuthModal";
+import Button from "@/components/Button";
import { BlindboxListSkeleton } from "@/components/Skeleton";
import type { UserProfile } from "@/types";
@@ -256,14 +256,15 @@ export default function BlindboxLobbyPage() {
maxLength={30}
className="h-11 flex-1 rounded-xl border-none bg-surface px-4 text-sm text-foreground outline-none ring-1 ring-border transition-all placeholder:text-dim focus:ring-2 focus:ring-purple-600"
/>
- }
>
- {creating ? : }
创建
-
+
{/* Join alternative */}
@@ -286,14 +287,16 @@ export default function BlindboxLobbyPage() {
maxLength={6}
className="h-11 flex-1 rounded-xl border-none bg-surface px-4 text-center font-mono text-sm tracking-[0.15em] text-foreground outline-none ring-1 ring-border transition-all placeholder:text-dim focus:ring-2 focus:ring-purple-600"
/>
- }
>
- {joining ? : }
加入
-
+
{error && (
@@ -330,14 +333,14 @@ export default function BlindboxLobbyPage() {
maxLength={30}
className="h-10 flex-1 rounded-xl border-none bg-surface px-3 text-sm text-foreground outline-none ring-1 ring-border transition-all placeholder:text-dim focus:ring-2 focus:ring-purple-600"
/>
- }
>
- {creating ? : }
创建
-
+
{/* Join row */}
@@ -354,14 +357,15 @@ export default function BlindboxLobbyPage() {
maxLength={6}
className="h-10 flex-1 rounded-xl border-none bg-surface px-3 text-center font-mono text-sm tracking-[0.15em] text-foreground outline-none ring-1 ring-border transition-all placeholder:font-sans placeholder:tracking-normal placeholder:text-dim focus:ring-2 focus:ring-purple-600"
/>
- }
>
- {joining ? : }
加入
-
+
{error && (
diff --git a/src/app/error.tsx b/src/app/error.tsx
index 0204dd2..0c34e22 100644
--- a/src/app/error.tsx
+++ b/src/app/error.tsx
@@ -3,6 +3,7 @@
import { useEffect } from "react";
import { motion } from "framer-motion";
import { AlertTriangle, RotateCcw, Home } from "lucide-react";
+import Button from "@/components/Button";
export default function Error({
error,
@@ -37,20 +38,12 @@ export default function Error({
diff --git a/src/app/invite/[id]/page.tsx b/src/app/invite/[id]/page.tsx
index 859a1fc..163f275 100644
--- a/src/app/invite/[id]/page.tsx
+++ b/src/app/invite/[id]/page.tsx
@@ -9,11 +9,11 @@ import {
Heart,
Sparkles,
ChevronRight,
- Loader2,
Coffee,
} from "lucide-react";
import { getUserId } from "@/lib/userId";
import { Skeleton, SkeletonCircle } from "@/components/Skeleton";
+import Button from "@/components/Button";
import { getSceneConfig } from "@/lib/sceneConfig";
import type { SceneType } from "@/types";
@@ -93,12 +93,9 @@ export default function InvitePage() {
这个房间已过期或不存在,请让朋友重新分享链接
-
);
}
@@ -194,20 +191,15 @@ export default function InvitePage() {
animate={{ y: 0, opacity: 1 }}
transition={{ duration: 0.5, delay: 0.3 }}
>
-
- {joining ? (
- <>
-
- 加入中...
- >
- ) : (
- "加入房间"
- )}
-
+ 加入房间
+
);
diff --git a/src/app/room/[id]/page.tsx b/src/app/room/[id]/page.tsx
index 9b42599..5b095c1 100644
--- a/src/app/room/[id]/page.tsx
+++ b/src/app/room/[id]/page.tsx
@@ -6,6 +6,7 @@ import TopNav from "@/components/TopNav";
import SwipeDeck from "@/components/SwipeDeck";
import { SwipeDeckSkeleton } from "@/components/Skeleton";
import LeaveConfirmModal from "@/components/LeaveConfirmModal";
+import Button from "@/components/Button";
import { useRoomPolling } from "@/hooks/useRoomPolling";
import { getUserId } from "@/lib/userId";
import { getSceneConfig } from "@/lib/sceneConfig";
@@ -96,12 +97,9 @@ export default function RoomPage() {
🍜
房间不存在或已过期
房间号可能有误,或房间已超过 24 小时
- router.push("/")}
- className="mt-2 h-10 rounded-xl bg-accent px-6 text-sm font-bold text-white shadow-lg shadow-accent/20 transition-colors hover:bg-accent-hover"
- >
+ router.push("/")}>
返回首页
-
+
);
}
diff --git a/src/components/AuthModal.tsx b/src/components/AuthModal.tsx
index 0e39765..e24c397 100644
--- a/src/components/AuthModal.tsx
+++ b/src/components/AuthModal.tsx
@@ -2,11 +2,12 @@
import { useState } from "react";
import { motion } from "framer-motion";
-import { X, Loader2, Eye, EyeOff } from "lucide-react";
+import { X, Eye, EyeOff } from "lucide-react";
import { AVATARS } from "@/lib/avatars";
import { setCachedProfile } from "@/lib/userId";
import type { UserProfile } from "@/types";
import Modal from "@/components/Modal";
+import Button from "@/components/Button";
type Tab = "login" | "register";
@@ -232,22 +233,16 @@ export default function AuthModal({ open, onClose, onAuth, defaultTab = "login"
)}
-
- {loading ? (
- <>
-
- {tab === "login" ? "登录中..." : "注册中..."}
- >
- ) : tab === "login" ? (
- "登录"
- ) : (
- "注册"
- )}
-
+ {tab === "login" ? "登录" : "注册"}
+
);
}
diff --git a/src/components/Button.tsx b/src/components/Button.tsx
new file mode 100644
index 0000000..f19215a
--- /dev/null
+++ b/src/components/Button.tsx
@@ -0,0 +1,71 @@
+"use client";
+
+import { type ComponentProps, type ReactNode } from "react";
+import { motion } from "framer-motion";
+import { Loader2 } from "lucide-react";
+
+const variantStyles = {
+ primary:
+ "bg-accent text-white shadow-lg shadow-accent/20 hover:bg-accent-hover disabled:opacity-50",
+ secondary:
+ "bg-surface text-secondary ring-1 ring-border hover:bg-elevated disabled:opacity-40",
+ danger:
+ "bg-rose-600 text-white hover:bg-rose-500 disabled:opacity-50",
+ ghost:
+ "text-muted hover:text-secondary hover:bg-elevated disabled:opacity-50",
+ purple:
+ "bg-purple-600 text-white hover:bg-purple-500 disabled:opacity-50",
+} as const;
+
+const sizeStyles = {
+ sm: "h-8 px-3 text-xs gap-1",
+ md: "h-10 px-4 text-sm gap-1.5",
+ lg: "h-11 px-6 text-sm font-bold gap-2",
+} as const;
+
+const spinnerSize = { sm: 13, md: 15, lg: 18 } as const;
+
+interface ButtonProps
+ extends Omit, "ref" | "children"> {
+ variant?: keyof typeof variantStyles;
+ size?: "sm" | "md" | "lg";
+ shape?: "rounded" | "pill";
+ loading?: boolean;
+ loadingText?: string;
+ icon?: ReactNode;
+ fullWidth?: boolean;
+ children?: ReactNode;
+}
+
+export default function Button({
+ variant = "primary",
+ size = "md",
+ shape = "rounded",
+ loading = false,
+ loadingText,
+ icon,
+ fullWidth = false,
+ className = "",
+ disabled,
+ children,
+ ...rest
+}: ButtonProps) {
+ const base = "flex items-center justify-center font-semibold transition-colors";
+ const shapeClass = shape === "pill" ? "rounded-full" : "rounded-xl";
+
+ return (
+
+ {loading ? (
+
+ ) : icon ? (
+ icon
+ ) : null}
+ {loading && loadingText ? loadingText : children}
+
+ );
+}
diff --git a/src/components/MatchResult.tsx b/src/components/MatchResult.tsx
index 9919a58..ba2e89c 100644
--- a/src/components/MatchResult.tsx
+++ b/src/components/MatchResult.tsx
@@ -34,6 +34,7 @@ import { isRegistered } from "@/lib/userId";
import ShareCardModal from "@/components/ShareCardModal";
import RestaurantImage from "@/components/RestaurantImage";
import AuthModal from "@/components/AuthModal";
+import Button from "@/components/Button";
import { useToast } from "@/hooks/useToast";
interface MatchResultProps {
@@ -107,24 +108,26 @@ function NoMatchResult({
animate={{ y: 0, opacity: 1 }}
transition={{ delay: 0.55 }}
>
- }
+ className="px-8 py-3"
>
-
- {resetting ? "重置中..." : "再来一轮"}
-
+ 再来一轮
+
- router.push("/")}
- className="flex items-center justify-center gap-2 rounded-full bg-surface px-8 py-3 text-sm font-bold text-muted ring-1 ring-border transition-colors hover:bg-elevated"
- whileTap={{ scale: 0.95 }}
+ variant="secondary"
+ shape="pill"
+ icon={}
+ className="px-8 py-3"
>
-
换个条件重新搜
-
+
);
@@ -487,14 +490,15 @@ export default function MatchResult({
)}
- }
+ className="px-8 py-3"
>
-
生成分享卡片
-
+
{/* Registration nudge */}
@@ -511,14 +515,14 @@ export default function MatchResult({
仅需用户名 + 密码,10 秒完成
- setShowAuth(true)}
- className="mt-3 flex h-10 w-full items-center justify-center gap-2 rounded-xl bg-accent text-sm font-bold text-white shadow-lg shadow-accent/20 transition-colors hover:bg-accent-hover"
- whileTap={{ scale: 0.95 }}
+ fullWidth
+ icon={}
+ className="mt-3"
>
-
注册保存记录
-
+
)}
diff --git a/src/components/QrInviteModal.tsx b/src/components/QrInviteModal.tsx
index 3f5f4ac..cb48300 100644
--- a/src/components/QrInviteModal.tsx
+++ b/src/components/QrInviteModal.tsx
@@ -6,6 +6,7 @@ import { X, Copy, Share2, QrCode } from "lucide-react";
import type { SceneType } from "@/types";
import { getSceneConfig } from "@/lib/sceneConfig";
import Modal from "@/components/Modal";
+import Button from "@/components/Button";
import { useToast } from "@/hooks/useToast";
interface QrInviteModalProps {
@@ -92,20 +93,23 @@ export default function QrInviteModal({
- }
+ className="flex-1"
>
-
复制链接
-
-
+ }
+ className="flex-1"
>
-
发送邀请
-
+