From 455b9e04d8f7cd4e1d4ec4451e3e960f0af52e02 Mon Sep 17 00:00:00 2001 From: kurihada Date: Thu, 26 Feb 2026 18:39:14 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E6=8F=90=E5=8F=96=20Button=20?= =?UTF-8?q?=E7=BB=84=E4=BB=B6=EF=BC=8C=E7=BB=9F=E4=B8=80=E6=8C=89=E9=92=AE?= =?UTF-8?q?=E5=8F=98=E4=BD=93=E3=80=81=E5=B0=BA=E5=AF=B8=E5=92=8C=E5=8A=A0?= =?UTF-8?q?=E8=BD=BD=E7=8A=B6=E6=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增 Button.tsx 支持 5 种变体(primary/secondary/danger/ghost/purple)、 3 种尺寸(sm/md/lg)、pill/rounded 形状及内置 loading 状态, 替换 8 个文件中 16 处重复的按钮样板代码。 --- src/app/blindbox/[code]/page.tsx | 30 +++++++------- src/app/blindbox/page.tsx | 46 +++++++++++---------- src/app/error.tsx | 17 +++----- src/app/invite/[id]/page.tsx | 28 +++++-------- src/app/room/[id]/page.tsx | 8 ++-- src/components/AuthModal.tsx | 25 +++++------ src/components/Button.tsx | 71 ++++++++++++++++++++++++++++++++ src/components/MatchResult.tsx | 48 +++++++++++---------- src/components/QrInviteModal.tsx | 20 +++++---- 9 files changed, 178 insertions(+), 115 deletions(-) create mode 100644 src/components/Button.tsx 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() { >

你还不是这个房间的成员

- + ) : ( <> @@ -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" /> - + {/* 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" /> - + {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" /> - + {/* 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" /> - + {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 }} > - + 加入房间 + ); 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 小时

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