refactor: 提取 Button 组件,统一按钮变体、尺寸和加载状态
新增 Button.tsx 支持 5 种变体(primary/secondary/danger/ghost/purple)、 3 种尺寸(sm/md/lg)、pill/rounded 形状及内置 loading 状态, 替换 8 个文件中 16 处重复的按钮样板代码。
This commit is contained in:
@@ -23,6 +23,7 @@ import {
|
|||||||
import confetti from "canvas-confetti";
|
import confetti from "canvas-confetti";
|
||||||
import { getCachedProfile, isRegistered } from "@/lib/userId";
|
import { getCachedProfile, isRegistered } from "@/lib/userId";
|
||||||
import ShareCardModal from "@/components/ShareCardModal";
|
import ShareCardModal from "@/components/ShareCardModal";
|
||||||
|
import Button from "@/components/Button";
|
||||||
import { useToast } from "@/hooks/useToast";
|
import { useToast } from "@/hooks/useToast";
|
||||||
import { BlindboxRoomSkeleton } from "@/components/Skeleton";
|
import { BlindboxRoomSkeleton } from "@/components/Skeleton";
|
||||||
import type { UserProfile } from "@/types";
|
import type { UserProfile } from "@/types";
|
||||||
@@ -536,14 +537,15 @@ export default function BlindboxRoomPage() {
|
|||||||
>
|
>
|
||||||
<Package size={40} className="text-purple-400/50" strokeWidth={1.5} />
|
<Package size={40} className="text-purple-400/50" strokeWidth={1.5} />
|
||||||
<p className="text-sm text-tertiary">你还不是这个房间的成员</p>
|
<p className="text-sm text-tertiary">你还不是这个房间的成员</p>
|
||||||
<button
|
<Button
|
||||||
onClick={handleJoinRoom}
|
onClick={handleJoinRoom}
|
||||||
disabled={joiningRoom}
|
variant="purple"
|
||||||
className="flex h-11 items-center gap-2 rounded-xl bg-purple-600 px-6 text-sm font-bold text-white transition-colors hover:bg-purple-500 disabled:opacity-50"
|
size="lg"
|
||||||
|
loading={joiningRoom}
|
||||||
|
icon={<LogIn size={16} />}
|
||||||
>
|
>
|
||||||
{joiningRoom ? <Loader2 size={16} className="animate-spin" /> : <LogIn size={16} />}
|
|
||||||
加入房间
|
加入房间
|
||||||
</button>
|
</Button>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
@@ -717,21 +719,21 @@ export default function BlindboxRoomPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<motion.button
|
<Button
|
||||||
onClick={() => setShowShareCard(true)}
|
onClick={() => 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"
|
variant="purple"
|
||||||
whileTap={{ scale: 0.96 }}
|
shape="pill"
|
||||||
|
icon={<Share2 size={14} />}
|
||||||
>
|
>
|
||||||
<Share2 size={14} />
|
|
||||||
分享契约
|
分享契约
|
||||||
</motion.button>
|
</Button>
|
||||||
<motion.button
|
<Button
|
||||||
onClick={() => { setPhase("pool"); setRevealedIdea(null); setShowShareCard(false); }}
|
onClick={() => { setPhase("pool"); setRevealedIdea(null); setShowShareCard(false); }}
|
||||||
className="flex h-10 items-center gap-2 rounded-full bg-surface px-5 text-xs font-semibold text-muted ring-1 ring-border transition-colors hover:bg-elevated"
|
variant="secondary"
|
||||||
whileTap={{ scale: 0.96 }}
|
shape="pill"
|
||||||
>
|
>
|
||||||
继续投入想法
|
继续投入想法
|
||||||
</motion.button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
+25
-21
@@ -10,11 +10,11 @@ import {
|
|||||||
LogIn,
|
LogIn,
|
||||||
Users,
|
Users,
|
||||||
Sparkles,
|
Sparkles,
|
||||||
Loader2,
|
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { getCachedProfile, isRegistered } from "@/lib/userId";
|
import { getCachedProfile, isRegistered } from "@/lib/userId";
|
||||||
import AuthModal from "@/components/AuthModal";
|
import AuthModal from "@/components/AuthModal";
|
||||||
|
import Button from "@/components/Button";
|
||||||
import { BlindboxListSkeleton } from "@/components/Skeleton";
|
import { BlindboxListSkeleton } from "@/components/Skeleton";
|
||||||
import type { UserProfile } from "@/types";
|
import type { UserProfile } from "@/types";
|
||||||
|
|
||||||
@@ -256,14 +256,15 @@ export default function BlindboxLobbyPage() {
|
|||||||
maxLength={30}
|
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"
|
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"
|
||||||
/>
|
/>
|
||||||
<button
|
<Button
|
||||||
onClick={handleCreate}
|
onClick={handleCreate}
|
||||||
disabled={creating}
|
variant="purple"
|
||||||
className="flex h-11 items-center gap-1.5 rounded-xl bg-purple-600 px-4 text-sm font-bold text-white transition-colors hover:bg-purple-500 disabled:opacity-50"
|
size="lg"
|
||||||
|
loading={creating}
|
||||||
|
icon={<Plus size={16} />}
|
||||||
>
|
>
|
||||||
{creating ? <Loader2 size={16} className="animate-spin" /> : <Plus size={16} />}
|
|
||||||
创建
|
创建
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Join alternative */}
|
{/* Join alternative */}
|
||||||
@@ -286,14 +287,16 @@ export default function BlindboxLobbyPage() {
|
|||||||
maxLength={6}
|
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"
|
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"
|
||||||
/>
|
/>
|
||||||
<button
|
<Button
|
||||||
onClick={handleJoin}
|
onClick={handleJoin}
|
||||||
disabled={joining || joinCode.trim().length < 6}
|
variant="secondary"
|
||||||
className="flex h-11 items-center gap-1.5 rounded-xl bg-surface px-4 text-sm font-semibold text-secondary ring-1 ring-border transition-colors hover:bg-elevated disabled:opacity-40"
|
size="lg"
|
||||||
|
disabled={joinCode.trim().length < 6}
|
||||||
|
loading={joining}
|
||||||
|
icon={<LogIn size={16} />}
|
||||||
>
|
>
|
||||||
{joining ? <Loader2 size={16} className="animate-spin" /> : <LogIn size={16} />}
|
|
||||||
加入
|
加入
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
@@ -330,14 +333,14 @@ export default function BlindboxLobbyPage() {
|
|||||||
maxLength={30}
|
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"
|
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"
|
||||||
/>
|
/>
|
||||||
<button
|
<Button
|
||||||
onClick={handleCreate}
|
onClick={handleCreate}
|
||||||
disabled={creating}
|
variant="purple"
|
||||||
className="flex h-10 items-center gap-1.5 rounded-xl bg-purple-600 px-4 text-xs font-bold text-white transition-colors hover:bg-purple-500 disabled:opacity-50"
|
loading={creating}
|
||||||
|
icon={<Plus size={14} />}
|
||||||
>
|
>
|
||||||
{creating ? <Loader2 size={14} className="animate-spin" /> : <Plus size={14} />}
|
|
||||||
创建
|
创建
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Join row */}
|
{/* Join row */}
|
||||||
@@ -354,14 +357,15 @@ export default function BlindboxLobbyPage() {
|
|||||||
maxLength={6}
|
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"
|
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"
|
||||||
/>
|
/>
|
||||||
<button
|
<Button
|
||||||
onClick={handleJoin}
|
onClick={handleJoin}
|
||||||
disabled={joining || joinCode.trim().length < 6}
|
variant="secondary"
|
||||||
className="flex h-10 items-center gap-1.5 rounded-xl bg-surface px-4 text-xs font-semibold text-secondary ring-1 ring-border transition-colors hover:bg-elevated disabled:opacity-40"
|
disabled={joinCode.trim().length < 6}
|
||||||
|
loading={joining}
|
||||||
|
icon={<LogIn size={14} />}
|
||||||
>
|
>
|
||||||
{joining ? <Loader2 size={14} className="animate-spin" /> : <LogIn size={14} />}
|
|
||||||
加入
|
加入
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
|
|||||||
+5
-12
@@ -3,6 +3,7 @@
|
|||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { motion } from "framer-motion";
|
import { motion } from "framer-motion";
|
||||||
import { AlertTriangle, RotateCcw, Home } from "lucide-react";
|
import { AlertTriangle, RotateCcw, Home } from "lucide-react";
|
||||||
|
import Button from "@/components/Button";
|
||||||
|
|
||||||
export default function Error({
|
export default function Error({
|
||||||
error,
|
error,
|
||||||
@@ -37,20 +38,12 @@ export default function Error({
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className="mt-8 flex gap-3">
|
<div className="mt-8 flex gap-3">
|
||||||
<button
|
<Button onClick={reset} variant="danger" icon={<RotateCcw size={15} />}>
|
||||||
onClick={reset}
|
|
||||||
className="flex items-center gap-1.5 rounded-xl bg-rose-600 px-5 py-2.5 text-sm font-semibold text-white transition-colors hover:bg-rose-500"
|
|
||||||
>
|
|
||||||
<RotateCcw size={15} />
|
|
||||||
重试
|
重试
|
||||||
</button>
|
</Button>
|
||||||
<a
|
<Button onClick={() => window.location.href = "/"} variant="secondary" icon={<Home size={15} />}>
|
||||||
href="/"
|
|
||||||
className="flex items-center gap-1.5 rounded-xl bg-surface px-5 py-2.5 text-sm font-semibold text-secondary ring-1 ring-border transition-colors hover:bg-elevated"
|
|
||||||
>
|
|
||||||
<Home size={15} />
|
|
||||||
首页
|
首页
|
||||||
</a>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -9,11 +9,11 @@ import {
|
|||||||
Heart,
|
Heart,
|
||||||
Sparkles,
|
Sparkles,
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
Loader2,
|
|
||||||
Coffee,
|
Coffee,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { getUserId } from "@/lib/userId";
|
import { getUserId } from "@/lib/userId";
|
||||||
import { Skeleton, SkeletonCircle } from "@/components/Skeleton";
|
import { Skeleton, SkeletonCircle } from "@/components/Skeleton";
|
||||||
|
import Button from "@/components/Button";
|
||||||
import { getSceneConfig } from "@/lib/sceneConfig";
|
import { getSceneConfig } from "@/lib/sceneConfig";
|
||||||
import type { SceneType } from "@/types";
|
import type { SceneType } from "@/types";
|
||||||
|
|
||||||
@@ -93,12 +93,9 @@ export default function InvitePage() {
|
|||||||
<p className="text-center text-sm text-muted">
|
<p className="text-center text-sm text-muted">
|
||||||
这个房间已过期或不存在,请让朋友重新分享链接
|
这个房间已过期或不存在,请让朋友重新分享链接
|
||||||
</p>
|
</p>
|
||||||
<button
|
<Button onClick={() => router.push("/")} className="mt-2">
|
||||||
onClick={() => router.push("/")}
|
|
||||||
className="mt-2 rounded-xl bg-accent px-6 py-2.5 text-sm font-bold text-white shadow-lg shadow-accent/20 transition-colors hover:bg-accent-hover"
|
|
||||||
>
|
|
||||||
自己创建房间
|
自己创建房间
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -194,20 +191,15 @@ export default function InvitePage() {
|
|||||||
animate={{ y: 0, opacity: 1 }}
|
animate={{ y: 0, opacity: 1 }}
|
||||||
transition={{ duration: 0.5, delay: 0.3 }}
|
transition={{ duration: 0.5, delay: 0.3 }}
|
||||||
>
|
>
|
||||||
<button
|
<Button
|
||||||
onClick={handleJoin}
|
onClick={handleJoin}
|
||||||
disabled={joining}
|
size="lg"
|
||||||
className="flex h-12 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 disabled:opacity-50"
|
fullWidth
|
||||||
|
loading={joining}
|
||||||
|
loadingText="加入中..."
|
||||||
>
|
>
|
||||||
{joining ? (
|
加入房间
|
||||||
<>
|
</Button>
|
||||||
<Loader2 size={18} className="animate-spin" />
|
|
||||||
加入中...
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
"加入房间"
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import TopNav from "@/components/TopNav";
|
|||||||
import SwipeDeck from "@/components/SwipeDeck";
|
import SwipeDeck from "@/components/SwipeDeck";
|
||||||
import { SwipeDeckSkeleton } from "@/components/Skeleton";
|
import { SwipeDeckSkeleton } from "@/components/Skeleton";
|
||||||
import LeaveConfirmModal from "@/components/LeaveConfirmModal";
|
import LeaveConfirmModal from "@/components/LeaveConfirmModal";
|
||||||
|
import Button from "@/components/Button";
|
||||||
import { useRoomPolling } from "@/hooks/useRoomPolling";
|
import { useRoomPolling } from "@/hooks/useRoomPolling";
|
||||||
import { getUserId } from "@/lib/userId";
|
import { getUserId } from "@/lib/userId";
|
||||||
import { getSceneConfig } from "@/lib/sceneConfig";
|
import { getSceneConfig } from "@/lib/sceneConfig";
|
||||||
@@ -96,12 +97,9 @@ export default function RoomPage() {
|
|||||||
<p className="text-4xl">🍜</p>
|
<p className="text-4xl">🍜</p>
|
||||||
<p className="text-base font-semibold text-secondary">房间不存在或已过期</p>
|
<p className="text-base font-semibold text-secondary">房间不存在或已过期</p>
|
||||||
<p className="text-sm text-muted">房间号可能有误,或房间已超过 24 小时</p>
|
<p className="text-sm text-muted">房间号可能有误,或房间已超过 24 小时</p>
|
||||||
<button
|
<Button onClick={() => router.push("/")}>
|
||||||
onClick={() => 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"
|
|
||||||
>
|
|
||||||
返回首页
|
返回首页
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,11 +2,12 @@
|
|||||||
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { motion } from "framer-motion";
|
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 { AVATARS } from "@/lib/avatars";
|
||||||
import { setCachedProfile } from "@/lib/userId";
|
import { setCachedProfile } from "@/lib/userId";
|
||||||
import type { UserProfile } from "@/types";
|
import type { UserProfile } from "@/types";
|
||||||
import Modal from "@/components/Modal";
|
import Modal from "@/components/Modal";
|
||||||
|
import Button from "@/components/Button";
|
||||||
|
|
||||||
type Tab = "login" | "register";
|
type Tab = "login" | "register";
|
||||||
|
|
||||||
@@ -232,22 +233,16 @@ export default function AuthModal({ open, onClose, onAuth, defaultTab = "login"
|
|||||||
</motion.p>
|
</motion.p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<button
|
<Button
|
||||||
onClick={handleSubmit}
|
onClick={handleSubmit}
|
||||||
disabled={loading}
|
size="lg"
|
||||||
className="mt-5 flex h-11 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 disabled:opacity-50"
|
fullWidth
|
||||||
|
loading={loading}
|
||||||
|
loadingText={tab === "login" ? "登录中..." : "注册中..."}
|
||||||
|
className="mt-5"
|
||||||
>
|
>
|
||||||
{loading ? (
|
{tab === "login" ? "登录" : "注册"}
|
||||||
<>
|
</Button>
|
||||||
<Loader2 size={16} className="animate-spin" />
|
|
||||||
{tab === "login" ? "登录中..." : "注册中..."}
|
|
||||||
</>
|
|
||||||
) : tab === "login" ? (
|
|
||||||
"登录"
|
|
||||||
) : (
|
|
||||||
"注册"
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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<ComponentProps<typeof motion.button>, "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 (
|
||||||
|
<motion.button
|
||||||
|
className={`${base} ${variantStyles[variant]} ${sizeStyles[size]} ${shapeClass} ${fullWidth ? "w-full" : ""} ${className}`}
|
||||||
|
disabled={loading || disabled}
|
||||||
|
whileTap={{ scale: 0.95 }}
|
||||||
|
{...rest}
|
||||||
|
>
|
||||||
|
{loading ? (
|
||||||
|
<Loader2 size={spinnerSize[size]} className="animate-spin" />
|
||||||
|
) : icon ? (
|
||||||
|
icon
|
||||||
|
) : null}
|
||||||
|
{loading && loadingText ? loadingText : children}
|
||||||
|
</motion.button>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -34,6 +34,7 @@ import { isRegistered } from "@/lib/userId";
|
|||||||
import ShareCardModal from "@/components/ShareCardModal";
|
import ShareCardModal from "@/components/ShareCardModal";
|
||||||
import RestaurantImage from "@/components/RestaurantImage";
|
import RestaurantImage from "@/components/RestaurantImage";
|
||||||
import AuthModal from "@/components/AuthModal";
|
import AuthModal from "@/components/AuthModal";
|
||||||
|
import Button from "@/components/Button";
|
||||||
import { useToast } from "@/hooks/useToast";
|
import { useToast } from "@/hooks/useToast";
|
||||||
|
|
||||||
interface MatchResultProps {
|
interface MatchResultProps {
|
||||||
@@ -107,24 +108,26 @@ function NoMatchResult({
|
|||||||
animate={{ y: 0, opacity: 1 }}
|
animate={{ y: 0, opacity: 1 }}
|
||||||
transition={{ delay: 0.55 }}
|
transition={{ delay: 0.55 }}
|
||||||
>
|
>
|
||||||
<motion.button
|
<Button
|
||||||
onClick={onReset}
|
onClick={onReset}
|
||||||
disabled={resetting}
|
shape="pill"
|
||||||
className="flex items-center justify-center gap-2 rounded-full bg-accent px-8 py-3 text-sm font-bold text-white shadow-lg shadow-accent/20 transition-colors hover:bg-accent-hover disabled:opacity-50"
|
loading={resetting}
|
||||||
whileTap={{ scale: 0.95 }}
|
loadingText="重置中..."
|
||||||
|
icon={<RotateCcw size={15} />}
|
||||||
|
className="px-8 py-3"
|
||||||
>
|
>
|
||||||
<RotateCcw size={15} className={resetting ? "animate-spin" : ""} />
|
再来一轮
|
||||||
{resetting ? "重置中..." : "再来一轮"}
|
</Button>
|
||||||
</motion.button>
|
|
||||||
|
|
||||||
<motion.button
|
<Button
|
||||||
onClick={() => router.push("/")}
|
onClick={() => 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"
|
variant="secondary"
|
||||||
whileTap={{ scale: 0.95 }}
|
shape="pill"
|
||||||
|
icon={<Home size={15} />}
|
||||||
|
className="px-8 py-3"
|
||||||
>
|
>
|
||||||
<Home size={15} />
|
|
||||||
换个条件重新搜
|
换个条件重新搜
|
||||||
</motion.button>
|
</Button>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
);
|
);
|
||||||
@@ -487,14 +490,15 @@ export default function MatchResult({
|
|||||||
</motion.a>
|
</motion.a>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<motion.button
|
<Button
|
||||||
onClick={handleOpenShareCard}
|
onClick={handleOpenShareCard}
|
||||||
className="flex items-center justify-center gap-2 rounded-full bg-surface px-8 py-3 text-sm font-bold text-secondary ring-1 ring-border transition-colors hover:bg-elevated"
|
variant="secondary"
|
||||||
whileTap={{ scale: 0.95 }}
|
shape="pill"
|
||||||
|
icon={<Share2 size={15} />}
|
||||||
|
className="px-8 py-3"
|
||||||
>
|
>
|
||||||
<Share2 size={15} />
|
|
||||||
生成分享卡片
|
生成分享卡片
|
||||||
</motion.button>
|
</Button>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
{/* Registration nudge */}
|
{/* Registration nudge */}
|
||||||
@@ -511,14 +515,14 @@ export default function MatchResult({
|
|||||||
<p className="mt-1 text-xs text-muted">
|
<p className="mt-1 text-xs text-muted">
|
||||||
仅需用户名 + 密码,10 秒完成
|
仅需用户名 + 密码,10 秒完成
|
||||||
</p>
|
</p>
|
||||||
<motion.button
|
<Button
|
||||||
onClick={() => setShowAuth(true)}
|
onClick={() => 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"
|
fullWidth
|
||||||
whileTap={{ scale: 0.95 }}
|
icon={<UserPlus size={15} />}
|
||||||
|
className="mt-3"
|
||||||
>
|
>
|
||||||
<UserPlus size={15} />
|
|
||||||
注册保存记录
|
注册保存记录
|
||||||
</motion.button>
|
</Button>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { X, Copy, Share2, QrCode } from "lucide-react";
|
|||||||
import type { SceneType } from "@/types";
|
import type { SceneType } from "@/types";
|
||||||
import { getSceneConfig } from "@/lib/sceneConfig";
|
import { getSceneConfig } from "@/lib/sceneConfig";
|
||||||
import Modal from "@/components/Modal";
|
import Modal from "@/components/Modal";
|
||||||
|
import Button from "@/components/Button";
|
||||||
import { useToast } from "@/hooks/useToast";
|
import { useToast } from "@/hooks/useToast";
|
||||||
|
|
||||||
interface QrInviteModalProps {
|
interface QrInviteModalProps {
|
||||||
@@ -92,20 +93,23 @@ export default function QrInviteModal({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-5 flex w-full gap-2.5">
|
<div className="mt-5 flex w-full gap-2.5">
|
||||||
<button
|
<Button
|
||||||
onClick={handleCopy}
|
onClick={handleCopy}
|
||||||
className="flex h-11 flex-1 items-center justify-center gap-1.5 rounded-xl bg-elevated text-sm font-semibold text-secondary ring-1 ring-border transition-colors active:bg-subtle"
|
variant="secondary"
|
||||||
|
size="lg"
|
||||||
|
icon={<Copy size={15} />}
|
||||||
|
className="flex-1"
|
||||||
>
|
>
|
||||||
<Copy size={15} />
|
|
||||||
复制链接
|
复制链接
|
||||||
</button>
|
</Button>
|
||||||
<button
|
<Button
|
||||||
onClick={handleShare}
|
onClick={handleShare}
|
||||||
className="flex h-11 flex-1 items-center justify-center gap-1.5 rounded-xl bg-accent text-sm font-semibold text-white shadow-lg shadow-accent/20 transition-colors active:bg-accent-hover"
|
size="lg"
|
||||||
|
icon={<Share2 size={15} />}
|
||||||
|
className="flex-1"
|
||||||
>
|
>
|
||||||
<Share2 size={15} />
|
|
||||||
发送邀请
|
发送邀请
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
Reference in New Issue
Block a user